Skip to content

Commit bf47ead

Browse files
committed
Add security validations for headers and payloads in auth plugins
- Introduced MAX_HEADER_VALUE_LENGTH and MAX_PAYLOAD_SIZE constants for validation. - Implemented header and payload size validation methods in Base class. - Updated HMAC and SharedSecret validators to utilize new validation methods. - Added tests for oversized headers and payloads to ensure compliance with limits.
1 parent 3197630 commit bf47ead

File tree

4 files changed

+104
-48
lines changed

4 files changed

+104
-48
lines changed

lib/hooks/plugins/auth/base.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ module Auth
1414
class Base
1515
extend Hooks::Core::ComponentAccess
1616

17+
# Security constants shared across auth validators
18+
MAX_HEADER_VALUE_LENGTH = ENV.fetch("HOOKS_MAX_HEADER_VALUE_LENGTH", 1024).to_i # Prevent DoS attacks via large header values
19+
MAX_PAYLOAD_SIZE = ENV.fetch("HOOKS_MAX_PAYLOAD_SIZE", 10 * 1024 * 1024).to_i # 10MB limit for payload validation
20+
1721
# Validate request
1822
#
1923
# @param payload [String] Raw request body
@@ -67,6 +71,61 @@ def self.find_header_value(headers, header_name)
6771
end
6872
nil
6973
end
74+
75+
# Validate headers object for security issues
76+
#
77+
# @param headers [Object] Headers to validate
78+
# @return [Boolean] true if headers are valid
79+
def self.valid_headers?(headers)
80+
unless headers.respond_to?(:each)
81+
log.warn("Auth validation failed: Invalid headers object")
82+
return false
83+
end
84+
true
85+
end
86+
87+
# Validate payload size for security issues
88+
#
89+
# @param payload [String] Payload to validate
90+
# @return [Boolean] true if payload is valid
91+
def self.valid_payload_size?(payload)
92+
return true if payload.nil?
93+
94+
if payload.bytesize > MAX_PAYLOAD_SIZE
95+
log.warn("Auth validation failed: Payload size exceeds maximum limit of #{MAX_PAYLOAD_SIZE} bytes")
96+
return false
97+
end
98+
true
99+
end
100+
101+
# Validate header value for security issues
102+
#
103+
# @param header_value [String] Header value to validate
104+
# @param header_name [String] Header name for logging
105+
# @return [Boolean] true if header value is valid
106+
def self.valid_header_value?(header_value, header_name)
107+
return false if header_value.nil? || header_value.empty?
108+
109+
# Check length to prevent DoS
110+
if header_value.length > MAX_HEADER_VALUE_LENGTH
111+
log.warn("Auth validation failed: #{header_name} exceeds maximum length")
112+
return false
113+
end
114+
115+
# Check for whitespace tampering
116+
if header_value != header_value.strip
117+
log.warn("Auth validation failed: #{header_name} contains leading/trailing whitespace")
118+
return false
119+
end
120+
121+
# Check for control characters
122+
if header_value.match?(/[\u0000-\u001f\u007f-\u009f]/)
123+
log.warn("Auth validation failed: #{header_name} contains control characters")
124+
return false
125+
end
126+
127+
true
128+
end
70129
end
71130
end
72131
end

lib/hooks/plugins/auth/hmac.rb

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ module Auth
4848
class HMAC < Base
4949
# Security constants
5050
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
5251

5352
# Default configuration values for HMAC validation
5453
#
@@ -114,43 +113,22 @@ def self.valid?(payload:, headers:, config:)
114113

115114
validator_config = build_config(config)
116115

117-
# Security: Check raw headers BEFORE normalization to detect tampering
118-
unless headers.respond_to?(:each)
119-
log.warn("Auth::HMAC validation failed: Invalid headers object")
120-
return false
121-
end
116+
# Security: Check raw headers and payload BEFORE processing
117+
return false unless valid_headers?(headers)
118+
return false unless valid_payload_size?(payload)
122119

123120
signature_header = validator_config[:header]
124121

125122
# Find the signature header with case-insensitive matching
126123
raw_signature = find_header_value(headers, signature_header)
127124

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-
133125
if raw_signature.nil? || raw_signature.empty?
134126
log.warn("Auth::HMAC validation failed: Missing or empty signature header '#{signature_header}'")
135127
return false
136128
end
137129

138-
# Security: Reject signatures with leading/trailing whitespace
139-
if raw_signature != raw_signature.strip
140-
log.warn("Auth::HMAC validation failed: Signature contains leading/trailing whitespace")
141-
return false
142-
end
143-
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-
149-
# Security: Reject signatures containing null bytes or other control characters
150-
if raw_signature.match?(/[\u0000-\u001f\u007f-\u009f]/)
151-
log.warn("Auth::HMAC validation failed: Signature contains control characters")
152-
return false
153-
end
130+
# Validate signature format using shared validation but with HMAC-specific length limit
131+
return false unless validate_signature_format(raw_signature)
154132

155133
# Now we can safely normalize headers for the rest of the validation
156134
normalized_headers = normalize_headers(headers)
@@ -210,6 +188,22 @@ def self.valid?(payload:, headers:, config:)
210188

211189
private
212190

191+
# Validate signature format for HMAC (uses HMAC-specific length limit)
192+
#
193+
# @param signature [String] Raw signature to validate
194+
# @return [Boolean] true if signature is valid
195+
# @api private
196+
def self.validate_signature_format(signature)
197+
# Check signature length with HMAC-specific limit
198+
if signature.length > MAX_SIGNATURE_LENGTH
199+
log.warn("Auth::HMAC validation failed: Signature length exceeds maximum limit of #{MAX_SIGNATURE_LENGTH} characters")
200+
return false
201+
end
202+
203+
# Use shared validation for other checks
204+
valid_header_value?(signature, "Signature")
205+
end
206+
213207
# Build final configuration by merging defaults with provided config
214208
#
215209
# Combines default configuration values with user-provided settings,

lib/hooks/plugins/auth/shared_secret.rb

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,9 @@ def self.valid?(payload:, headers:, config:)
6161

6262
validator_config = build_config(config)
6363

64-
# Security: Check raw headers BEFORE normalization to detect tampering
65-
unless headers.respond_to?(:each)
66-
log.warn("Auth::SharedSecret validation failed: Invalid headers object")
67-
return false
68-
end
64+
# Security: Check raw headers and payload BEFORE processing
65+
return false unless valid_headers?(headers)
66+
return false unless valid_payload_size?(payload)
6967

7068
secret_header = validator_config[:header]
7169

@@ -77,19 +75,13 @@ def self.valid?(payload:, headers:, config:)
7775
return false
7876
end
7977

80-
stripped_secret = raw_secret.strip
81-
82-
# Security: Reject secrets with leading/trailing whitespace
83-
if raw_secret != stripped_secret
84-
log.warn("Auth::SharedSecret validation failed: Secret contains leading/trailing whitespace")
78+
# Validate secret format using shared validation
79+
unless valid_header_value?(raw_secret, "Secret")
80+
log.warn("Auth::SharedSecret validation failed: Invalid secret format")
8581
return false
8682
end
8783

88-
# Security: Reject secrets containing null bytes or other control characters
89-
if raw_secret.match?(/[\u0000-\u001f\u007f-\u009f]/)
90-
log.warn("Auth::SharedSecret validation failed: Secret contains control characters")
91-
return false
92-
end
84+
stripped_secret = raw_secret.strip
9385

9486
# Use secure comparison to prevent timing attacks
9587
result = Rack::Utils.secure_compare(secret, stripped_secret)
@@ -106,12 +98,6 @@ def self.valid?(payload:, headers:, config:)
10698

10799
private
108100

109-
# Short logger accessor for auth module
110-
# @return [Hooks::Log] Logger instance
111-
def self.log
112-
Hooks::Log.instance
113-
end
114-
115101
# Build final configuration by merging defaults with provided config
116102
#
117103
# Combines default configuration values with user-provided settings,

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def valid_with(args = {})
2626
end
2727

2828
before do
29+
allow(ENV).to receive(:[]).and_call_original
2930
allow(ENV).to receive(:[]).with("SUPER_WEBHOOK_SECRET").and_return(secret)
3031
end
3132

@@ -99,6 +100,22 @@ def valid_with(args = {})
99100
nil_headers = { default_header => nil }
100101
expect(valid_with(headers: nil_headers)).to be false
101102
end
103+
104+
it "returns false for secrets exceeding maximum length limit" do
105+
# Create secret larger than MAX_HEADER_VALUE_LENGTH (1024 + 1 characters)
106+
oversized_secret = "a" * (1024 + 1)
107+
oversized_headers = { default_header => oversized_secret }
108+
expect(log).to receive(:warn).with(/exceeds maximum length/)
109+
expect(valid_with(headers: oversized_headers)).to be false
110+
end
111+
112+
it "returns false for payloads exceeding maximum size limit" do
113+
# Create payload larger than MAX_PAYLOAD_SIZE (10MB + 1 byte)
114+
oversized_payload = "a" * (10 * 1024 * 1024 + 1)
115+
headers = { default_header => secret }
116+
expect(log).to receive(:warn).with(/Payload size exceeds maximum limit/)
117+
expect(valid_with(payload: oversized_payload, headers: headers)).to be false
118+
end
102119
end
103120

104121
context "with custom header configuration" do

0 commit comments

Comments
 (0)