SSRF protection update to work with ssrf_filter 1.3, explicitly pin gem version

This commit is contained in:
Markus Kahl
2026-03-10 16:12:40 +00:00
parent 5ac08d5caa
commit f64e45eec7
9 changed files with 28 additions and 25 deletions
+1
View File
@@ -201,6 +201,7 @@ gem "nokogiri", "~> 1.19.1"
gem "carrierwave", "~> 2.2.6"
gem "carrierwave_direct", "~> 3.0.0"
gem "ssrf_filter", "~> 1.3"
gem "fog-aws"
gem "aws-sdk-core", "~> 3.241"
+1
View File
@@ -1753,6 +1753,7 @@ DEPENDENCIES
spring-commands-rubocop
sprockets (~> 3.7.2)
sprockets-rails (~> 3.5.1)
ssrf_filter (~> 1.3)
stackprof
statesman (~> 13.1.0)
store_attribute (~> 2.0)
@@ -43,7 +43,7 @@ RSpec.describe Webhooks::Outgoing::RequestWebhookService, :webmock, type: :model
describe "#call!" do
context "when the request is successful" do
before do
stub_request(:post, ssrf_resolved_url(webhook.url))
stub_request(:post, webhook.url)
.with(body: "body", headers: { "X-Custom" => "header" })
.to_return(status: 200, body: "OK", headers: { "Content-Type" => "application/json" })
end
@@ -52,7 +52,7 @@ RSpec.describe Webhooks::Outgoing::RequestWebhookService, :webmock, type: :model
it "makes a POST request to the webhook URL with the given body and headers" do
subject
expect(WebMock).to have_requested(:post, ssrf_resolved_url(webhook.url))
expect(WebMock).to have_requested(:post, webhook.url)
.with(body: "body", headers: { "X-Custom" => "header", "Host" => "example.net" }).once
end
@@ -67,11 +67,24 @@ RSpec.describe Webhooks::Outgoing::RequestWebhookService, :webmock, type: :model
expect(log.response_body).to eq("OK")
expect(log.url).to eq(webhook.url)
end
it "connects to the original hostname while routing through the resolved safe IP address" do
http_start_args = nil
allow(Net::HTTP).to receive(:start).and_wrap_original do |original, *args, **kwargs, &block|
http_start_args = { host: args[0], options: kwargs }
original.call(*args, **kwargs, &block)
end
subject
expect(http_start_args[:host]).to eq("example.net")
expect(http_start_args[:options]).to include(ipaddr: WithSsrfWebhookStubsMixin::SSRF_TEST_IP)
end
end
context "when the request times out" do
before do
stub_request(:post, ssrf_resolved_url(webhook.url)).to_timeout
stub_request(:post, webhook.url).to_timeout
end
it "re-raises the timeout error while still creating a log entry" do
@@ -83,7 +96,7 @@ RSpec.describe Webhooks::Outgoing::RequestWebhookService, :webmock, type: :model
context "when request_url fails with SSL errors" do
before do
stub_request(:post, ssrf_resolved_url(webhook.url)).to_raise(OpenSSL::SSL::SSLError)
stub_request(:post, webhook.url).to_raise(OpenSSL::SSL::SSLError)
end
it "still logs the exception" do
@@ -135,7 +148,7 @@ RSpec.describe Webhooks::Outgoing::RequestWebhookService, :webmock, type: :model
context "when an unexpected error occurs" do
before do
stub_request(:post, ssrf_resolved_url(webhook.url)).to_raise(StandardError.new("something went wrong"))
stub_request(:post, webhook.url).to_raise(StandardError.new("something went wrong"))
end
it "creates a log entry" do
@@ -52,7 +52,7 @@ RSpec.describe AttachmentWebhookJob, :webmock, type: :job do
end
let(:stub) do
stub_request(:post, ssrf_resolved_url(stubbed_url))
stub_request(:post, stubbed_url)
.with(
body: hash_including(
"action" => event,
@@ -56,7 +56,7 @@ RSpec.describe ProjectWebhookJob, :webmock, type: :job do
end
let(:stub) do
stub_request(:post, ssrf_resolved_url(stubbed_url))
stub_request(:post, stubbed_url)
.with(
body: hash_including(
"action" => event,
@@ -53,7 +53,7 @@ RSpec.describe TimeEntryWebhookJob, :webmock, type: :job do
end
let(:stub) do
stub_request(:post, ssrf_resolved_url(stubbed_url))
stub_request(:post, stubbed_url)
.with(
body: hash_including(
"action" => event,
@@ -53,7 +53,7 @@ RSpec.describe WorkPackageCommentWebhookJob, :webmock, type: :model do
end
let(:stub) do
stub_request(:post, ssrf_resolved_url(stubbed_url)).with(
stub_request(:post, stubbed_url).with(
body: hash_including(
"action" => event_name,
"activity" => hash_including(
@@ -56,7 +56,7 @@ RSpec.describe WorkPackageWebhookJob, :webmock, type: :model do
end
let(:stub) do
stub_request(:post, ssrf_resolved_url(stubbed_url))
stub_request(:post, stubbed_url)
.with(
body: hash_including(
"action" => event,
+3 -15
View File
@@ -29,23 +29,11 @@
module WithSsrfWebhookStubsMixin
##
# A safe public IP returned by the stubbed resolver for any hostname.
# It is not in SsrfFilter's private-address blocklist, so SSRF validation passes,
# and WebMock stubs using this IP will match the actual Net::HTTP request.
# It is not in SsrfFilter's private-address blocklist, so SSRF validation passes.
# ssrf_filter 1.3+ makes requests to the original hostname URL (not the resolved IP),
# passing the resolved IP via the `ipaddr:` option to Net::HTTP.start instead.
SSRF_TEST_IP = "93.184.216.34"
##
# Translates a webhook URL containing a hostname to the IP-based URL that
# SsrfFilter will use when making the actual HTTP request. Use this when
# setting up WebMock stubs so that they match the resolved request.
#
# URLs that already contain an IP address are returned unchanged.
def ssrf_resolved_url(url)
uri = URI.parse(url)
return url if ip_address?(uri.host)
url.sub(uri.host, SSRF_TEST_IP)
end
def ip_address?(host)
[Resolv::IPv4::Regex, Resolv::IPv6::Regex].any? { host.match? it }
end