Skip to content

Commit d1a7d6c

Browse files
authored
Merge pull request #12865 from demarche-numerique/US/webhook-support
Tech: sanitize webhook URLs
2 parents 7a7403a + 8a4f193 commit d1a7d6c

2 files changed

Lines changed: 19 additions & 58 deletions

File tree

app/jobs/web_hook_job.rb

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# frozen_string_literal: true
22

3-
require 'resolv'
43
require 'addressable/uri'
54

65
class WebHookJob < ApplicationJob
@@ -11,13 +10,6 @@ class WebHookJob < ApplicationJob
1110
def perform(procedure_id, dossier_id, state, updated_at)
1211
procedure = Procedure.find(procedure_id)
1312

14-
if unsafe_url?(procedure.web_hook_url)
15-
Sentry.set_tags(procedure: procedure_id, dossier: dossier_id)
16-
Sentry.set_extras(web_hook_url: procedure.web_hook_url)
17-
Sentry.capture_message("Webhook SSRF blocked: #{procedure.web_hook_url} resolves to a private IP")
18-
return
19-
end
20-
2113
body = {
2214
procedure_id: procedure_id,
2315
dossier_id: dossier_id,
@@ -29,25 +21,17 @@ def perform(procedure_id, dossier_id, state, updated_at)
2921

3022
if !response.success?
3123
Sentry.set_tags(procedure: procedure_id, dossier: dossier_id)
32-
Sentry.set_extras(web_hook_url: procedure.web_hook_url)
24+
Sentry.set_extras(web_hook_url: sanitized_url(procedure.web_hook_url))
3325
Sentry.capture_message("Webhook error code: #{response.code} (#{response.return_message}) // Response: #{response.body}")
3426
end
3527
end
3628

3729
private
3830

39-
def unsafe_url?(url)
40-
return true if url.blank?
41-
31+
def sanitized_url(url)
4232
uri = Addressable::URI.parse(url)
43-
host = uri&.host
44-
return true if host.blank?
45-
46-
addresses = Resolv.getaddresses(host)
47-
return true if addresses.empty?
48-
49-
addresses.any? { NoPrivateIPURLValidator.private_ip?(_1) }
50-
rescue Addressable::URI::InvalidURIError, Resolv::ResolvError
51-
true
33+
"#{uri.scheme}://#{uri.host}#{uri.path}"
34+
rescue Addressable::URI::InvalidURIError
35+
'(invalid URL)'
5236
end
5337
end

spec/jobs/web_hook_job_spec.rb

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,54 +15,31 @@
1515
end
1616

1717
context 'with error on webhook' do
18-
it 'raises' do
18+
it 'reports to Sentry with sanitized URL' do
19+
allow(Sentry).to receive(:set_tags)
20+
allow(Sentry).to receive(:set_extras)
1921
allow(Sentry).to receive(:capture_message)
2022
stub_request(:post, web_hook_url).to_return(status: 500, body: "error")
2123

2224
job.perform_now
25+
26+
expect(Sentry).to have_received(:set_extras).with(web_hook_url: "https://domaine.fr/callback_url")
2327
expect(Sentry).to have_received(:capture_message)
2428
end
2529
end
2630

27-
context 'SSRF protection' do
28-
context 'when webhook URL resolves to a private IP' do
29-
let(:web_hook_url) { "https://domaine.fr/callback_url" }
30-
31-
it 'does not make the HTTP request when DNS resolves to a private IP' do
32-
stub = stub_request(:post, web_hook_url).to_return(status: 200, body: "success")
33-
allow(Resolv).to receive(:getaddresses).with('domaine.fr').and_return(['10.0.0.1'])
34-
35-
job.perform_now
36-
expect(stub).not_to have_been_requested
37-
end
38-
39-
it 'does not make the HTTP request when DNS resolves to localhost' do
40-
stub = stub_request(:post, web_hook_url).to_return(status: 200, body: "success")
41-
allow(Resolv).to receive(:getaddresses).with('domaine.fr').and_return(['127.0.0.1'])
42-
43-
job.perform_now
44-
expect(stub).not_to have_been_requested
45-
end
46-
47-
it 'does not make the HTTP request when DNS resolves to link-local' do
48-
stub = stub_request(:post, web_hook_url).to_return(status: 200, body: "success")
49-
allow(Resolv).to receive(:getaddresses).with('domaine.fr').and_return(['169.254.169.254'])
31+
context 'when webhook URL contains query params' do
32+
let(:web_hook_url) { "https://domaine.fr/callback_url?token=secret123" }
5033

51-
job.perform_now
52-
expect(stub).not_to have_been_requested
53-
end
54-
end
55-
56-
context 'when webhook URL resolves to a public IP' do
57-
let(:web_hook_url) { "https://domaine.fr/callback_url" }
34+
it 'strips query params from Sentry logs' do
35+
allow(Sentry).to receive(:set_tags)
36+
allow(Sentry).to receive(:set_extras)
37+
allow(Sentry).to receive(:capture_message)
38+
stub_request(:post, web_hook_url).to_return(status: 500, body: "error")
5839

59-
it 'makes the HTTP request normally' do
60-
stub = stub_request(:post, web_hook_url).to_return(status: 200, body: "success")
61-
allow(Resolv).to receive(:getaddresses).with('domaine.fr').and_return(['93.184.216.34'])
40+
job.perform_now
6241

63-
job.perform_now
64-
expect(stub).to have_been_requested
65-
end
42+
expect(Sentry).to have_received(:set_extras).with(web_hook_url: "https://domaine.fr/callback_url")
6643
end
6744
end
6845
end

0 commit comments

Comments
 (0)