Skip to content

Commit 3197630

Browse files
committed
add MAX_PAYLOAD_SIZE and MAX_SIGNATURE_LENGTH checks to the default hmac auth plugin
1 parent ab00fdf commit 3197630

File tree

4 files changed

+35
-1
lines changed

4 files changed

+35
-1
lines changed

.rubocop.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ AllCops:
1515
GitHub/InsecureHashAlgorithm:
1616
Exclude:
1717
- "spec/unit/lib/hooks/plugins/auth/hmac_spec.rb"
18+
- "spec/acceptance/acceptance_tests.rb"
1819

1920
GitHub/AvoidObjectSendWithDynamicMethod:
2021
Exclude:

docs/design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Core configuration options can be provided via environment variables:
100100
export HOOKS_CONFIG=./config/config.yaml
101101

102102
# Runtime settings (override config file)
103-
export HOOKS_REQUEST_LIMIT=1048576
103+
export HOOKS_REQUEST_LIMIT=1048576 # 1 MB
104104
export HOOKS_REQUEST_TIMEOUT=15
105105
export HOOKS_GRACEFUL_SHUTDOWN_TIMEOUT=30
106106
export HOOKS_ROOT_PATH="/webhooks"

lib/hooks/plugins/auth/hmac.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ module Auth
4646
# payload_template: "{timestamp}.{body}"
4747
# timestamp_tolerance: 300 # 5 minutes
4848
class HMAC < Base
49+
# Security constants
50+
MAX_SIGNATURE_LENGTH = ENV.fetch("HOOKS_MAX_SIGNATURE_LENGTH", 1024).to_i # Prevent DoS attacks via large signatures
51+
MAX_PAYLOAD_SIZE = ENV.fetch("MAX_PAYLOAD_SIZE", 10 * 1024 * 1024).to_i # 10MB limit for payload validation
52+
4953
# Default configuration values for HMAC validation
5054
#
5155
# @return [Hash<Symbol, String|Integer>] Default configuration settings
@@ -121,6 +125,11 @@ def self.valid?(payload:, headers:, config:)
121125
# Find the signature header with case-insensitive matching
122126
raw_signature = find_header_value(headers, signature_header)
123127

128+
if payload && payload.bytesize > MAX_PAYLOAD_SIZE
129+
log.warn("Auth::HMAC validation failed: Payload size exceeds maximum limit of #{MAX_PAYLOAD_SIZE} bytes")
130+
return false
131+
end
132+
124133
if raw_signature.nil? || raw_signature.empty?
125134
log.warn("Auth::HMAC validation failed: Missing or empty signature header '#{signature_header}'")
126135
return false
@@ -132,6 +141,11 @@ def self.valid?(payload:, headers:, config:)
132141
return false
133142
end
134143

144+
if raw_signature.length > MAX_SIGNATURE_LENGTH
145+
log.warn("Auth::HMAC validation failed: Signature length exceeds maximum limit of #{MAX_SIGNATURE_LENGTH} characters")
146+
return false
147+
end
148+
135149
# Security: Reject signatures containing null bytes or other control characters
136150
if raw_signature.match?(/[\u0000-\u001f\u007f-\u009f]/)
137151
log.warn("Auth::HMAC validation failed: Signature contains control characters")

spec/unit/lib/hooks/plugins/auth/hmac_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
before(:each) do
2525
Hooks::Log.instance = log
26+
allow(ENV).to receive(:[]).and_call_original
2627
allow(ENV).to receive(:[]).with("HMAC_TEST_SECRET").and_return(secret)
2728
end
2829

@@ -273,16 +274,34 @@ def create_timestamped_signature(timestamp, version = "v0")
273274
it "handles very long signatures gracefully" do
274275
long_signature = "sha256=" + ("a" * 10000)
275276
long_headers = { default_header => long_signature }
277+
expect(log).to receive(:warn).with(/Signature length exceeds maximum limit/)
276278
expect(valid_with(headers: long_headers)).to be false
277279
end
278280

281+
it "returns false for signatures exceeding maximum length limit" do
282+
# Create signature larger than MAX_SIGNATURE_LENGTH (1024 + 1 characters)
283+
oversized_signature = "sha256=" + ("a" * (1024 - 7 + 1)) # -7 for "sha256=" prefix
284+
oversized_headers = { default_header => oversized_signature }
285+
expect(log).to receive(:warn).with(/Signature length exceeds maximum limit/)
286+
expect(valid_with(headers: oversized_headers)).to be false
287+
end
288+
279289
it "handles very long payloads" do
280290
long_payload = "a" * 100000
281291
long_signature = create_algorithm_prefixed_signature(long_payload)
282292
long_headers = { default_header => long_signature }
283293
expect(valid_with(payload: long_payload, headers: long_headers)).to be true
284294
end
285295

296+
it "returns false for payloads exceeding maximum size limit" do
297+
# Create payload larger than MAX_PAYLOAD_SIZE (10MB + 1 byte)
298+
oversized_payload = "a" * (10 * 1024 * 1024 + 1)
299+
signature = create_algorithm_prefixed_signature(payload) # Use regular payload for signature
300+
headers_with_signature = { default_header => signature }
301+
expect(log).to receive(:warn).with(/Payload size exceeds maximum limit/)
302+
expect(valid_with(payload: oversized_payload, headers: headers_with_signature)).to be false
303+
end
304+
286305
it "returns false and logs for signature containing non-null control characters" do
287306
control_char = "\x01"
288307
headers_with_control = { default_header => signature + control_char }

0 commit comments

Comments
 (0)