Skip to content

Commit 150858a

Browse files
committed
feat(webhook whitelist): allow hosts to be whitelisted using * domains
Closes: pact-foundation#223
1 parent 296c924 commit 150858a

File tree

3 files changed

+69
-9
lines changed

3 files changed

+69
-9
lines changed

lib/pact_broker/doc/views/webhooks.markdown

+4-2
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,17 @@ Pact Broker Github repository.
123123

124124
* **Host**: If the `webhook_host_whitelist` contains any entries, the host must match one or more of the entries. By default, it is empty. For security purposes, if the host whitelist is empty, the response details will not be logged to the UI (though they can be seen in the application logs at debug level).
125125

126-
The host whitelist may contain hostnames (eg `"github.com"`), IPs (eg `"192.0.345.4"`), network ranges (eg `"10.0.0.0/8"`) or regular expressions (eg `/.*\.foo\.com$/`). Note that IPs are not resolved, so if you specify an IP range, you need to use the IP in the webhook URL. If you wish to allow webhooks to any host (not recommended!), you can set `webhook_host_whitelist` to `[/.*/]`. Beware of any sensitive endpoints that may be exposed within the same network.
126+
The host whitelist may contain hostnames (eg `"github.com"`), domains beginning with `*` (eg. `"*.foo.com"`), IPs (eg `"192.0.345.4"`), network ranges (eg `"10.0.0.0/8"`) or regular expressions (eg `/.*\.foo\.com$/`). Note that IPs are not resolved, so if you specify an IP range, you need to use the IP in the webhook URL. If you wish to allow webhooks to any host (not recommended!), you can set `webhook_host_whitelist` to `[/.*/]`. Beware of any sensitive endpoints that may be exposed within the same network.
127127

128128
The recommended set of values to start with are:
129129

130130
* your CI server's hostname (for triggering builds)
131131
* your company chat (eg. Slack, for publishing notifications)
132132
* your code repository (eg. Github, for sending commit statuses)
133133

134-
Alternatively, you could use a regular expression to limit requests to your company's domain. eg `/.*\.foo\.com$/` (don't forget the end of string anchor). You can test Ruby regular expressions at [rubular.com](http://rubular.com).
134+
Alternatively, you could use a domain beginning with a `*` to limit requests to your company's domain.
135+
136+
Note that the hostname/domain matching follows that used for SSL certificate hostnames, so `*.foo.com` will match `a.foo.com` but not `a.b.foo.com`. If you need more flexible matching because you have domains with variable "parts" (eg `a.b.foo.com`), you can use a regular expression (eg `/.*\.foo\.com$/` - don't forget the end of string anchor). You can test Ruby regular expressions at [rubular.com](http://rubular.com).
135137

136138
### Testing
137139

Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'openssl'
2+
13
module PactBroker
24
module Webhooks
35
class CheckHostWhitelist
@@ -7,16 +9,34 @@ def self.call(host, whitelist = PactBroker.configuration.webhook_host_whitelist)
79
end
810

911
def self.match?(host, whitelist_host)
10-
if whitelist_host.is_a?(Regexp)
11-
host =~ whitelist_host
12+
if parse_ip_address(host)
13+
ip_address_matches_range(host, whitelist_host)
14+
elsif whitelist_host.is_a?(Regexp)
15+
host_matches_regexp(host, whitelist_host)
16+
elsif whitelist_host.start_with?("*")
17+
OpenSSL::SSL.verify_hostname(host, whitelist_host)
1218
else
13-
begin
14-
IPAddr.new(whitelist_host) === IPAddr.new(host)
15-
rescue IPAddr::Error
16-
host == whitelist_host
17-
end
19+
host == whitelist_host
1820
end
1921
end
22+
23+
def self.parse_ip_address(addr)
24+
IPAddr.new(addr)
25+
rescue IPAddr::Error
26+
nil
27+
end
28+
29+
def self.ip_address_matches_range(host, maybe_whitelist_range)
30+
parse_ip_address(maybe_whitelist_range) === parse_ip_address(host)
31+
end
32+
33+
def self.host_matches_regexp(host, whitelist_regexp)
34+
host =~ whitelist_regexp
35+
end
36+
37+
def self.host_matches_domain_with_wildcard(host, whitelist_domain)
38+
OpenSSL::SSL.verify_hostname(host, whitelist_domain)
39+
end
2040
end
2141
end
2242
end

spec/lib/pact_broker/webhooks/check_host_whitelist_spec.rb

+38
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,44 @@ module Webhooks
2323
end
2424
end
2525

26+
context "when the whitelist includes *.foo.bar" do
27+
let(:whitelist) { ["*.foo.bar"] }
28+
29+
it "matches host a.foo.bar" do
30+
expect(CheckHostWhitelist.call("a.foo.bar", whitelist)).to eq whitelist
31+
end
32+
33+
it "does not matche host a.b.foo.bar" do
34+
expect(CheckHostWhitelist.call("a.b.foo.bar", whitelist)).to eq []
35+
end
36+
37+
it "does not match a.foo.bar.b" do
38+
expect(CheckHostWhitelist.call("a.foo.bar.b", whitelist)).to eq []
39+
end
40+
41+
it "does not match foo.bar" do
42+
expect(CheckHostWhitelist.call("foo.bar", whitelist)).to eq []
43+
end
44+
45+
it "does not match 10.0.0.2" do
46+
expect(CheckHostWhitelist.call("10.0.0.2", whitelist)).to eq []
47+
end
48+
end
49+
50+
context "when the whitelist includes *.2" do
51+
it "does not match 10.0.0.2 as that's the wrong way to declare an IP range" do
52+
expect(CheckHostWhitelist.call("10.0.0.2", ["*.0.0.2"])).to eq []
53+
end
54+
end
55+
56+
context "when the whitelist includes *.foo.*.bar" do
57+
let(:whitelist) { ["*.foo.*.bar"] }
58+
59+
it "does not match host a.foo.b.bar, according to RFC 6125, section 6.4.3, subitem 1" do
60+
expect(CheckHostWhitelist.call("a.foo.b.bar", whitelist)).to eq []
61+
end
62+
end
63+
2664
context "when the host is localhost" do
2765
let(:host) { "localhost" }
2866

0 commit comments

Comments
 (0)