Skip to content

Commit d7a2b0a

Browse files
committed
feat: validate webhook scheme and http method against configurable lists on creation
1 parent 6b0df51 commit d7a2b0a

File tree

7 files changed

+109
-11
lines changed

7 files changed

+109
-11
lines changed

lib/pact_broker/api/contracts/webhook_contract.rb

+55-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ module Api
66
module Contracts
77
class WebhookContract < Reform::Form
88

9+
def validate(*)
10+
result = super
11+
# I just cannot seem to get the validation to stop on the first error.
12+
# If one rule fails, they all come back failed, and it's driving me nuts.
13+
# Why on earth would I want that behaviour?
14+
new_errors = Reform::Contract::Errors.new
15+
errors.messages.each do | key, value |
16+
new_errors.add(key, value.first)
17+
end
18+
@errors = new_errors
19+
result
20+
end
21+
922
validation do
1023
configure do
1124
config.messages_file = File.expand_path("../../../locale/en.yml", __FILE__)
@@ -23,20 +36,56 @@ class WebhookContract < Reform::Form
2336
configure do
2437
config.messages_file = File.expand_path("../../../locale/en.yml", __FILE__)
2538

26-
def valid_method?(value)
27-
Net::HTTP.const_defined?(value.capitalize)
39+
def self.messages
40+
super.merge(
41+
en: {
42+
errors: {
43+
allowed_webhook_method?: http_method_error_message,
44+
allowed_webhook_scheme?: scheme_error_message
45+
}
46+
}
47+
)
48+
end
49+
50+
def self.http_method_error_message
51+
if PactBroker.configuration.webhook_http_method_whitelist.size == 1
52+
"must be #{PactBroker.configuration.webhook_http_method_whitelist.first}. See /doc/webhooks#whitelist for more information."
53+
else
54+
"must be one of #{PactBroker.configuration.webhook_http_method_whitelist.join(", ")}. See /doc/webhooks#whitelist for more information."
55+
end
56+
end
57+
58+
def self.scheme_error_message
59+
"must be #{PactBroker.configuration.webhook_scheme_whitelist.join(", ")}. See /doc/webhooks#whitelist for more information."
2860
end
2961

30-
def valid_url?(value)
31-
uri = URI(value)
62+
def valid_method?(http_method)
63+
Net::HTTP.const_defined?(http_method.capitalize)
64+
end
65+
66+
def valid_url?(url)
67+
uri = URI(url)
3268
uri.scheme && uri.host
3369
rescue URI::InvalidURIError
3470
false
3571
end
72+
73+
def allowed_webhook_method?(http_method)
74+
PactBroker.configuration.webhook_http_method_whitelist.any? do | allowed_method |
75+
http_method.downcase == allowed_method.downcase
76+
end
77+
end
78+
79+
def allowed_webhook_scheme?(url)
80+
scheme = URI(url).scheme
81+
PactBroker.configuration.webhook_scheme_whitelist.any? do | allowed_scheme |
82+
scheme.downcase == allowed_scheme.downcase
83+
end
84+
end
3685
end
3786

38-
required(:http_method).filled(:valid_method?)
39-
required(:url).filled(:valid_url?)
87+
required(:http_method).filled(:valid_method?, :allowed_webhook_method?)
88+
required(:url).filled(:valid_url?, :allowed_webhook_scheme?)
4089
end
4190
end
4291

lib/pact_broker/configuration.rb

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ 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
3334
attr_accessor :semver_formats
3435
attr_accessor :enable_public_badge_access, :shields_io_base_url
3536
attr_accessor :webhook_retry_schedule
@@ -74,6 +75,8 @@ def self.default_configuration
7475
config.webhook_retry_schedule = [10, 60, 120, 300, 600, 1200] #10 sec, 1 min, 2 min, 5 min, 10 min, 20 min => 38 minutes
7576
config.check_for_potential_duplicate_pacticipant_names = true
7677
config.disable_ssl_verification = false
78+
config.webhook_http_method_whitelist = ['POST']
79+
config.webhook_scheme_whitelist = ['https']
7780
config
7881
end
7982

lib/pact_broker/doc/views/webhooks.markdown

+9
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ A request body can be specified as well.
5454

5555
**BEWARE** The password can be reverse engineered from the database, so make a separate account for the Pact Broker to use, don't use your personal account!
5656

57+
<a name="whitelist"></a>
58+
#### Webhook Whitelist
59+
60+
To ensure that webhooks cannot be used maliciously to expose either data about your contracts or your internal network, the following validation rules are applied to webhooks via the Pact Broker configuration settings.
61+
62+
* **Scheme**: Must be included in the `webhook_scheme_whitelist`, which by default only includes `https`. You can change this to include `http` if absolutely necessary, however, keep in mind that the body of any http traffic is visible to the network. You can load a self signed certificate into the Pact Broker to be used for https connections using `script/insert-self-signed-certificate-from-url.rb` in the Pact Broker repository.
63+
64+
* **HTTP method**: Must be included in the `webhook_http_method_whitelist`, which by default only includes `POST`. It is highly recommended that only `POST` requests are allowed to ensure that webhooks cannot be used to retrieve sensitive information from hosts within the same network.
65+
5766
#### Event types
5867

5968
`contract_content_changed:` triggered when the content of the contract has changed since the previous publication. Uses plain string equality, so changes to the ordering of hash keys, or whitespace changes will trigger this webhook.

spec/features/create_webhook_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
}],
4444
request: {
4545
method: 'POST',
46-
url: 'http://example.org',
46+
url: 'https://example.org',
4747
headers: {
4848
:"Content-Type" => "application/json"
4949
},

spec/features/edit_webhook_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
let(:response_body) { JSON.parse(last_response.body, symbolize_names: true)}
1515
let(:webhook_json) do
1616
h = load_json_fixture('webhook_valid.json')
17-
h['request']['method'] = 'GET'
17+
h['request']['url'] = 'https://bar.com'
1818
h.to_json
1919
end
2020

@@ -55,7 +55,7 @@
5555

5656
it "updates the webhook" do
5757
subject
58-
expect(reloaded_webhook.request.method).to eq 'GET'
58+
expect(reloaded_webhook.request.url).to eq 'https://bar.com'
5959
end
6060
end
6161
end

spec/fixtures/webhook_valid.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
}],
55
"request": {
66
"method": "POST",
7-
"url": "http://some.url",
7+
"url": "HTTPS://some.url",
88
"username": "username",
99
"password": "password",
1010
"headers": {

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

+38-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module Contracts
1010
let(:hash) { JSON.parse(json) }
1111
let(:webhook) { PactBroker::Api::Decorators::WebhookDecorator.new(Domain::Webhook.new).from_json(json) }
1212
let(:subject) { WebhookContract.new(webhook) }
13+
let(:matching_hosts) { ['foo'] }
1314

1415
def valid_webhook_with
1516
hash = load_json_fixture 'webhook_valid.json'
@@ -18,11 +19,13 @@ def valid_webhook_with
1819
end
1920

2021
describe "errors" do
21-
2222
before do
23+
PactBroker.configuration.webhook_http_method_whitelist = webhook_http_method_whitelist
2324
subject.validate(hash)
2425
end
2526

27+
let(:webhook_http_method_whitelist) { ['POST'] }
28+
2629
context "with valid fields" do
2730
it "is empty" do
2831
expect(subject.errors).to be_empty
@@ -81,6 +84,40 @@ def valid_webhook_with
8184
end
8285
end
8386

87+
context "with an invalid scheme" do
88+
let(:json) do
89+
valid_webhook_with do |hash|
90+
hash['request']['url'] = 'ftp://foo'
91+
end
92+
end
93+
94+
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."
96+
end
97+
end
98+
99+
context "with an HTTP method that is not whitelisted" do
100+
let(:json) do
101+
valid_webhook_with do |hash|
102+
hash['request']['method'] = 'DELETE'
103+
end
104+
end
105+
106+
context "when there is only one allowed HTTP method" do
107+
it "contains an error for invalid method" do
108+
expect(subject.errors[:"request.http_method"]).to include "must be POST. See /doc/webhooks#whitelist for more information."
109+
end
110+
end
111+
112+
context "when there is more than one allowed HTTP method", pending: "need to work out how to dynamically create this message" do
113+
let(:webhook_http_method_whitelist) { ['POST', 'GET'] }
114+
115+
it "contains an error for invalid method" do
116+
expect(subject.errors[:"request.http_method"]).to include "must be one of POST, GET"
117+
end
118+
end
119+
end
120+
84121
context "with no URL" do
85122
let(:json) do
86123
valid_webhook_with do |hash|

0 commit comments

Comments
 (0)