Skip to content

Commit 077e37f

Browse files
committed
feat: validate webhook host against configurable list on creation
1 parent d7a2b0a commit 077e37f

File tree

5 files changed

+120
-6
lines changed

5 files changed

+120
-6
lines changed

lib/pact_broker/api/contracts/webhook_contract.rb

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'reform'
22
require 'reform/form'
3+
require 'pact_broker/webhooks/check_host_whitelist'
34

45
module PactBroker
56
module Api
@@ -41,7 +42,8 @@ def self.messages
4142
en: {
4243
errors: {
4344
allowed_webhook_method?: http_method_error_message,
44-
allowed_webhook_scheme?: scheme_error_message
45+
allowed_webhook_scheme?: scheme_error_message,
46+
allowed_webhook_host?: host_error_message
4547
}
4648
}
4749
)
@@ -56,7 +58,11 @@ def self.http_method_error_message
5658
end
5759

5860
def self.scheme_error_message
59-
"must be #{PactBroker.configuration.webhook_scheme_whitelist.join(", ")}. See /doc/webhooks#whitelist for more information."
61+
"scheme must be #{PactBroker.configuration.webhook_scheme_whitelist.join(", ")}. See /doc/webhooks#whitelist for more information."
62+
end
63+
64+
def self.host_error_message
65+
"host must be in the whitelist #{PactBroker.configuration.webhook_host_whitelist.join(",")}. See /doc/webhooks#whitelist for more information."
6066
end
6167

6268
def valid_method?(http_method)
@@ -82,10 +88,22 @@ def allowed_webhook_scheme?(url)
8288
scheme.downcase == allowed_scheme.downcase
8389
end
8490
end
91+
92+
def allowed_webhook_host?(url)
93+
if host_whitelist.any?
94+
PactBroker::Webhooks::CheckHostWhitelist.call(URI(url).host, host_whitelist).any?
95+
else
96+
true
97+
end
98+
end
99+
100+
def host_whitelist
101+
PactBroker.configuration.webhook_host_whitelist
102+
end
85103
end
86104

87105
required(:http_method).filled(:valid_method?, :allowed_webhook_method?)
88-
required(:url).filled(:valid_url?, :allowed_webhook_scheme?)
106+
required(:url).filled(:valid_url?, :allowed_webhook_scheme?, :allowed_webhook_host?)
89107
end
90108
end
91109

lib/pact_broker/configuration.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ class Configuration
3030
attr_accessor :validate_database_connection_config, :enable_diagnostic_endpoints, :version_parser, :sha_generator
3131
attr_accessor :use_case_sensitive_resource_names, :order_versions_by_date
3232
attr_accessor :check_for_potential_duplicate_pacticipant_names
33-
attr_accessor :webhook_http_method_whitelist, :webhook_scheme_whitelist
33+
attr_accessor :webhook_http_method_whitelist, :webhook_scheme_whitelist, :webhook_host_whitelist
34+
attr_accessor :webhook_retry_schedule
3435
attr_accessor :semver_formats
3536
attr_accessor :enable_public_badge_access, :shields_io_base_url
36-
attr_accessor :webhook_retry_schedule
3737
attr_accessor :disable_ssl_verification
3838
attr_accessor :base_equality_only_on_content_that_affects_verification_results
3939
attr_reader :api_error_reporters
@@ -77,6 +77,7 @@ def self.default_configuration
7777
config.disable_ssl_verification = false
7878
config.webhook_http_method_whitelist = ['POST']
7979
config.webhook_scheme_whitelist = ['https']
80+
config.webhook_host_whitelist = []
8081
config
8182
end
8283

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module PactBroker
2+
module Webhooks
3+
class CheckHostWhitelist
4+
5+
def self.call(host, whitelist = PactBroker.configuration.webhook_host_whitelist)
6+
whitelist.select{ | whitelist_host | match?(host, whitelist_host) }
7+
end
8+
9+
def self.match?(host, whitelist_host)
10+
if whitelist_host.is_a?(Regexp)
11+
host =~ whitelist_host
12+
else
13+
begin
14+
IPAddr.new(whitelist_host) === IPAddr.new(host)
15+
rescue IPAddr::Error
16+
host == whitelist_host
17+
end
18+
end
19+
end
20+
end
21+
end
22+
end

spec/lib/pact_broker/api/contracts/webhook_contract_spec.rb

+27-1
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ def valid_webhook_with
2121
describe "errors" do
2222
before do
2323
PactBroker.configuration.webhook_http_method_whitelist = webhook_http_method_whitelist
24+
PactBroker.configuration.webhook_host_whitelist = webhook_host_whitelist
25+
allow(PactBroker::Webhooks::CheckHostWhitelist).to receive(:call).and_return(whitelist_matches)
2426
subject.validate(hash)
2527
end
2628

2729
let(:webhook_http_method_whitelist) { ['POST'] }
30+
let(:whitelist_matches) { ['foo'] }
31+
let(:webhook_host_whitelist) { [] }
2832

2933
context "with valid fields" do
3034
it "is empty" do
@@ -92,7 +96,7 @@ def valid_webhook_with
9296
end
9397

9498
it "contains an error for the URL" do
95-
expect(subject.errors[:"request.url"]).to include "must be https. See /doc/webhooks#whitelist for more information."
99+
expect(subject.errors[:"request.url"]).to include "scheme must be https. See /doc/webhooks#whitelist for more information."
96100
end
97101
end
98102

@@ -118,6 +122,28 @@ def valid_webhook_with
118122
end
119123
end
120124

125+
context "when the host whitelist is empty" do
126+
it "does not attempt to validate the host" do
127+
expect(PactBroker::Webhooks::CheckHostWhitelist).to_not have_received(:call)
128+
end
129+
end
130+
131+
context "when the host whitelist is populated" do
132+
let(:webhook_host_whitelist) { [/foo/, "bar"] }
133+
134+
it "validates the host" do
135+
expect(PactBroker::Webhooks::CheckHostWhitelist).to have_received(:call).with("some.url", webhook_host_whitelist)
136+
end
137+
138+
context "when the host does not match the whitelist" do
139+
let(:whitelist_matches) { [] }
140+
141+
it "contains an error", pending: "need to work out how to do dynamic messages" do
142+
expect(subject.errors[:"request.url"]).to include "host must be in the whitelist /foo/, \"bar\" . See /doc/webhooks#whitelist for more information."
143+
end
144+
end
145+
end
146+
121147
context "with no URL" do
122148
let(:json) do
123149
valid_webhook_with do |hash|
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require 'pact_broker/webhooks/check_host_whitelist'
2+
3+
module PactBroker
4+
module Webhooks
5+
describe CheckHostWhitelist do
6+
context "when the host is 10.0.0.7" do
7+
let(:host) { "10.0.1.0" }
8+
9+
it "matches 10.0.0.0/8" do
10+
expect(CheckHostWhitelist.call(host, ["10.0.0.0/8"])).to eq ["10.0.0.0/8"]
11+
end
12+
13+
it "matches 10.0.1.0" do
14+
expect(CheckHostWhitelist.call(host, [host])).to eq [host]
15+
end
16+
17+
it "does not match 10.0.0.2" do
18+
expect(CheckHostWhitelist.call(host, ["10.0.0.2"])).to eq []
19+
end
20+
21+
it "does not match 10.0.0.0/28" do
22+
expect(CheckHostWhitelist.call(host, ["10.0.0.0/28"])).to eq []
23+
end
24+
end
25+
26+
context "when the host is localhost" do
27+
let(:host) { "localhost" }
28+
29+
it "matches localhost" do
30+
expect(CheckHostWhitelist.call(host, [host])).to eq [host]
31+
end
32+
33+
it "matches /local.*/" do
34+
expect(CheckHostWhitelist.call(host, [/local*/])).to eq [/local*/]
35+
end
36+
37+
it "does not match foo" do
38+
expect(CheckHostWhitelist.call(host, ["foo"])).to eq []
39+
end
40+
41+
it "does not match /foo.*/" do
42+
expect(CheckHostWhitelist.call(host, [/foo*/])).to eq []
43+
end
44+
end
45+
end
46+
end
47+
end

0 commit comments

Comments
 (0)