hooks-ruby 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -6
- data/lib/hooks/app/api.rb +8 -0
- data/lib/hooks/app/helpers.rb +1 -1
- data/lib/hooks/app/rack_env_builder.rb +2 -0
- data/lib/hooks/core/config_validator.rb +10 -4
- data/lib/hooks/core/plugin_loader.rb +4 -2
- data/lib/hooks/plugins/auth/base.rb +67 -0
- data/lib/hooks/plugins/auth/hmac.rb +112 -28
- data/lib/hooks/plugins/auth/shared_secret.rb +7 -21
- data/lib/hooks/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: efc93a22af173a3085d2af45a0787501cf4e520e0aa917c7b4b8ee477a477820
|
4
|
+
data.tar.gz: 06c6177f4356d06e59ceaf2f7e9c62848752ee1a5d298981c2820c1732906424
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e4bed95c7e1f41cd7188847773b83e1d7e1a463122545cc5c0330348539f81b2ed254c9e7ff161b2922b8f064ec46174288b3557be4c2d63604405ab931adb0d
|
7
|
+
data.tar.gz: fe7cdde0b508f5bcf1e354f6c3b2583530ad27f27d63686712d05830581b849fc2d8e61cad5ba1dd8d4283e182b823625d4e164bd9fa4909ae6be7be3d0ce360
|
data/README.md
CHANGED
@@ -50,9 +50,11 @@ Here is a very high-level overview of how Hooks works:
|
|
50
50
|
```yaml
|
51
51
|
# file: config/endpoints/hello.yml
|
52
52
|
path: /hello
|
53
|
-
handler:
|
53
|
+
handler: my_custom_handler # This is a custom handler plugin you would define in the plugins/handlers directory (snake_case)
|
54
54
|
```
|
55
55
|
|
56
|
+
> Note: If your handler's class name is `MyCustomHandler`, you would define it in the `plugins/handlers/my_custom_handler.rb` file. The `handler` field in the endpoint configuration file should be the snake_case version of the class name. So if your handler class is `MyCustomHandler`, you would use `my_custom_handler` in the endpoint configuration file.
|
57
|
+
|
56
58
|
3. Now create a corresponding handler plugin in the `plugins/handlers` directory. Here is an example of a simple handler plugin:
|
57
59
|
|
58
60
|
```ruby
|
@@ -64,7 +66,7 @@ Here is a very high-level overview of how Hooks works:
|
|
64
66
|
# For this example, we will just return a success message
|
65
67
|
{
|
66
68
|
status: "success",
|
67
|
-
handler: "
|
69
|
+
handler: "my_custom_handler",
|
68
70
|
payload_received: payload,
|
69
71
|
timestamp: Time.now.utc.iso8601
|
70
72
|
}
|
@@ -208,16 +210,16 @@ Endpoint configurations are defined in the `config/endpoints` directory. Each en
|
|
208
210
|
```yaml
|
209
211
|
# file: config/endpoints/hello.yml
|
210
212
|
path: /hello # becomes /webhooks/hello based on the root_path in hooks.yml
|
211
|
-
handler:
|
213
|
+
handler: hello_handler # This is a custom handler plugin you would define in the plugins/handlers
|
212
214
|
```
|
213
215
|
|
214
216
|
```yaml
|
215
217
|
# file: config/endpoints/goodbye.yml
|
216
218
|
path: /goodbye # becomes /webhooks/goodbye based on the root_path in hooks.yml
|
217
|
-
handler:
|
219
|
+
handler: goodbye_handler # This is another custom handler plugin you would define in the plugins/handlers
|
218
220
|
|
219
221
|
auth:
|
220
|
-
type:
|
222
|
+
type: goodbye # This is a custom authentication plugin you would define in the plugins/auth
|
221
223
|
secret_env_key: GOODBYE_API_KEY # the name of the environment variable containing the secret
|
222
224
|
header: Authorization
|
223
225
|
|
@@ -255,7 +257,7 @@ class GoodbyeHandler < Hooks::Plugins::Handlers::Base
|
|
255
257
|
# Ditto for the goodbye endpoint
|
256
258
|
{
|
257
259
|
message: "goodbye webhook processed successfully",
|
258
|
-
handler: "
|
260
|
+
handler: "goodbye_handler",
|
259
261
|
timestamp: Time.now.utc.iso8601
|
260
262
|
}
|
261
263
|
end
|
data/lib/hooks/app/api.rb
CHANGED
@@ -4,6 +4,7 @@ require "grape"
|
|
4
4
|
require "json"
|
5
5
|
require "securerandom"
|
6
6
|
require_relative "helpers"
|
7
|
+
#require_relative "network/ip_filtering"
|
7
8
|
require_relative "auth/auth"
|
8
9
|
require_relative "rack_env_builder"
|
9
10
|
require_relative "../plugins/handlers/base"
|
@@ -82,6 +83,13 @@ module Hooks
|
|
82
83
|
plugin.on_request(rack_env)
|
83
84
|
end
|
84
85
|
|
86
|
+
# TODO: IP filtering before processing the request if defined
|
87
|
+
# If IP filtering is enabled at either global or endpoint level, run the filtering rules
|
88
|
+
# before processing the request
|
89
|
+
#if config[:ip_filtering] || endpoint_config[:ip_filtering]
|
90
|
+
#ip_filtering!(headers, endpoint_config, config, request_context, rack_env)
|
91
|
+
#end
|
92
|
+
|
85
93
|
enforce_request_limits(config, request_context)
|
86
94
|
request.body.rewind
|
87
95
|
raw_body = request.body.read
|
data/lib/hooks/app/helpers.rb
CHANGED
@@ -74,7 +74,7 @@ module Hooks
|
|
74
74
|
|
75
75
|
# Load handler class
|
76
76
|
#
|
77
|
-
# @param handler_class_name [String] The name of the handler
|
77
|
+
# @param handler_class_name [String] The name of the handler in snake_case (e.g., "github_handler")
|
78
78
|
# @return [Object] An instance of the loaded handler class
|
79
79
|
# @raise [StandardError] If handler cannot be found
|
80
80
|
def load_handler(handler_class_name)
|
@@ -72,6 +72,8 @@ module Hooks
|
|
72
72
|
end
|
73
73
|
|
74
74
|
# Add HTTP headers to the environment with proper Rack naming convention
|
75
|
+
# Note: This will generally add headers like HTTP_X_CUSTOM_HEADER. For example, the HTTP_X_FORWARDED_FOR
|
76
|
+
# is a common header that is used to pass the original client IP address through proxies.
|
75
77
|
#
|
76
78
|
# @param rack_env [Hash] Environment hash to modify
|
77
79
|
def add_http_headers(rack_env)
|
@@ -45,6 +45,11 @@ module Hooks
|
|
45
45
|
optional(:format).filled(:string)
|
46
46
|
optional(:version_prefix).filled(:string)
|
47
47
|
optional(:payload_template).filled(:string)
|
48
|
+
optional(:header_format).filled(:string)
|
49
|
+
optional(:signature_key).filled(:string)
|
50
|
+
optional(:timestamp_key).filled(:string)
|
51
|
+
optional(:structured_header_separator).filled(:string)
|
52
|
+
optional(:key_value_separator).filled(:string)
|
48
53
|
end
|
49
54
|
|
50
55
|
optional(:opts).hash
|
@@ -120,11 +125,12 @@ module Hooks
|
|
120
125
|
# Must not be empty or only whitespace
|
121
126
|
return false if handler_name.strip.empty?
|
122
127
|
|
123
|
-
# Must match
|
124
|
-
return false unless handler_name.match?(/\A[
|
128
|
+
# Must match strict snake_case pattern: starts with lowercase, no trailing/consecutive underscores
|
129
|
+
return false unless handler_name.match?(/\A[a-z][a-z0-9]*(?:_[a-z0-9]+)*\z/)
|
125
130
|
|
126
|
-
#
|
127
|
-
|
131
|
+
# Convert to PascalCase for security check (since DANGEROUS_CLASSES uses PascalCase)
|
132
|
+
pascal_case_name = handler_name.split("_").map(&:capitalize).join("")
|
133
|
+
return false if Hooks::Security::DANGEROUS_CLASSES.include?(pascal_case_name)
|
128
134
|
|
129
135
|
true
|
130
136
|
end
|
@@ -61,11 +61,13 @@ module Hooks
|
|
61
61
|
|
62
62
|
# Get handler plugin class by name
|
63
63
|
#
|
64
|
-
# @param handler_name [String] Name of the handler (e.g., "
|
64
|
+
# @param handler_name [String] Name of the handler in snake_case (e.g., "github_handler", "team_1_handler")
|
65
65
|
# @return [Class] The handler plugin class
|
66
66
|
# @raise [StandardError] if handler not found
|
67
67
|
def get_handler_plugin(handler_name)
|
68
|
-
|
68
|
+
# Convert snake_case to PascalCase for registry lookup
|
69
|
+
pascal_case_name = handler_name.split("_").map(&:capitalize).join("")
|
70
|
+
plugin_class = @handler_plugins[pascal_case_name]
|
69
71
|
|
70
72
|
unless plugin_class
|
71
73
|
raise StandardError, "Handler plugin '#{handler_name}' not found. Available handlers: #{@handler_plugins.keys.join(', ')}"
|
@@ -4,6 +4,7 @@ require "rack/utils"
|
|
4
4
|
require_relative "../../core/log"
|
5
5
|
require_relative "../../core/global_components"
|
6
6
|
require_relative "../../core/component_access"
|
7
|
+
require_relative "timestamp_validator"
|
7
8
|
|
8
9
|
module Hooks
|
9
10
|
module Plugins
|
@@ -14,6 +15,10 @@ module Hooks
|
|
14
15
|
class Base
|
15
16
|
extend Hooks::Core::ComponentAccess
|
16
17
|
|
18
|
+
# Security constants shared across auth validators
|
19
|
+
MAX_HEADER_VALUE_LENGTH = ENV.fetch("HOOKS_MAX_HEADER_VALUE_LENGTH", 1024).to_i # Prevent DoS attacks via large header values
|
20
|
+
MAX_PAYLOAD_SIZE = ENV.fetch("HOOKS_MAX_PAYLOAD_SIZE", 10 * 1024 * 1024).to_i # 10MB limit for payload validation
|
21
|
+
|
17
22
|
# Validate request
|
18
23
|
#
|
19
24
|
# @param payload [String] Raw request body
|
@@ -49,6 +54,13 @@ module Hooks
|
|
49
54
|
return secret.strip
|
50
55
|
end
|
51
56
|
|
57
|
+
# Get timestamp validator instance
|
58
|
+
#
|
59
|
+
# @return [TimestampValidator] Singleton timestamp validator instance
|
60
|
+
def self.timestamp_validator
|
61
|
+
TimestampValidator.new
|
62
|
+
end
|
63
|
+
|
52
64
|
# Find a header value by name with case-insensitive matching
|
53
65
|
#
|
54
66
|
# @param headers [Hash] HTTP headers from the request
|
@@ -67,6 +79,61 @@ module Hooks
|
|
67
79
|
end
|
68
80
|
nil
|
69
81
|
end
|
82
|
+
|
83
|
+
# Validate headers object for security issues
|
84
|
+
#
|
85
|
+
# @param headers [Object] Headers to validate
|
86
|
+
# @return [Boolean] true if headers are valid
|
87
|
+
def self.valid_headers?(headers)
|
88
|
+
unless headers.respond_to?(:each)
|
89
|
+
log.warn("Auth validation failed: Invalid headers object")
|
90
|
+
return false
|
91
|
+
end
|
92
|
+
true
|
93
|
+
end
|
94
|
+
|
95
|
+
# Validate payload size for security issues
|
96
|
+
#
|
97
|
+
# @param payload [String] Payload to validate
|
98
|
+
# @return [Boolean] true if payload is valid
|
99
|
+
def self.valid_payload_size?(payload)
|
100
|
+
return true if payload.nil?
|
101
|
+
|
102
|
+
if payload.bytesize > MAX_PAYLOAD_SIZE
|
103
|
+
log.warn("Auth validation failed: Payload size exceeds maximum limit of #{MAX_PAYLOAD_SIZE} bytes")
|
104
|
+
return false
|
105
|
+
end
|
106
|
+
true
|
107
|
+
end
|
108
|
+
|
109
|
+
# Validate header value for security issues
|
110
|
+
#
|
111
|
+
# @param header_value [String] Header value to validate
|
112
|
+
# @param header_name [String] Header name for logging
|
113
|
+
# @return [Boolean] true if header value is valid
|
114
|
+
def self.valid_header_value?(header_value, header_name)
|
115
|
+
return false if header_value.nil? || header_value.empty?
|
116
|
+
|
117
|
+
# Check length to prevent DoS
|
118
|
+
if header_value.length > MAX_HEADER_VALUE_LENGTH
|
119
|
+
log.warn("Auth validation failed: #{header_name} exceeds maximum length")
|
120
|
+
return false
|
121
|
+
end
|
122
|
+
|
123
|
+
# Check for whitespace tampering
|
124
|
+
if header_value != header_value.strip
|
125
|
+
log.warn("Auth validation failed: #{header_name} contains leading/trailing whitespace")
|
126
|
+
return false
|
127
|
+
end
|
128
|
+
|
129
|
+
# Check for control characters
|
130
|
+
if header_value.match?(/[\u0000-\u001f\u007f-\u009f]/)
|
131
|
+
log.warn("Auth validation failed: #{header_name} contains control characters")
|
132
|
+
return false
|
133
|
+
end
|
134
|
+
|
135
|
+
true
|
136
|
+
end
|
70
137
|
end
|
71
138
|
end
|
72
139
|
end
|
@@ -3,7 +3,6 @@
|
|
3
3
|
require "openssl"
|
4
4
|
require "time"
|
5
5
|
require_relative "base"
|
6
|
-
require_relative "timestamp_validator"
|
7
6
|
|
8
7
|
module Hooks
|
9
8
|
module Plugins
|
@@ -32,7 +31,23 @@ module Hooks
|
|
32
31
|
# format: "version=signature"
|
33
32
|
# version_prefix: "v0"
|
34
33
|
# payload_template: "{version}:{timestamp}:{body}"
|
34
|
+
#
|
35
|
+
# @example Configuration for Tailscale-style structured headers
|
36
|
+
# auth:
|
37
|
+
# type: HMAC
|
38
|
+
# secret_env_key: WEBHOOK_SECRET
|
39
|
+
# header: Tailscale-Webhook-Signature
|
40
|
+
# algorithm: sha256
|
41
|
+
# format: "signature_only"
|
42
|
+
# header_format: "structured"
|
43
|
+
# signature_key: "v1"
|
44
|
+
# timestamp_key: "t"
|
45
|
+
# payload_template: "{timestamp}.{body}"
|
46
|
+
# timestamp_tolerance: 300 # 5 minutes
|
35
47
|
class HMAC < Base
|
48
|
+
# Security constants
|
49
|
+
MAX_SIGNATURE_LENGTH = ENV.fetch("HOOKS_MAX_SIGNATURE_LENGTH", 1024).to_i # Prevent DoS attacks via large signatures
|
50
|
+
|
36
51
|
# Default configuration values for HMAC validation
|
37
52
|
#
|
38
53
|
# @return [Hash<Symbol, String|Integer>] Default configuration settings
|
@@ -42,7 +57,8 @@ module Hooks
|
|
42
57
|
format: "algorithm=signature", # Format: algorithm=hash
|
43
58
|
header: "X-Signature", # Default header containing the signature
|
44
59
|
timestamp_tolerance: 300, # 5 minutes tolerance for timestamp validation
|
45
|
-
version_prefix: "v0"
|
60
|
+
version_prefix: "v0", # Default version prefix for versioned signatures
|
61
|
+
header_format: "simple" # Header format: "simple" or "structured"
|
46
62
|
}.freeze
|
47
63
|
|
48
64
|
# Mapping of signature format strings to internal format symbols
|
@@ -75,6 +91,11 @@ module Hooks
|
|
75
91
|
# @option config [String] :format ('algorithm=signature') Signature format
|
76
92
|
# @option config [String] :version_prefix ('v0') Version prefix for versioned signatures
|
77
93
|
# @option config [String] :payload_template Template for payload construction
|
94
|
+
# @option config [String] :header_format ('simple') Header format: 'simple' or 'structured'
|
95
|
+
# @option config [String] :signature_key ('v1') Key for signature in structured headers
|
96
|
+
# @option config [String] :timestamp_key ('t') Key for timestamp in structured headers
|
97
|
+
# @option config [String] :structured_header_separator (',') Separator for structured headers
|
98
|
+
# @option config [String] :key_value_separator ('=') Separator for key-value pairs in structured headers
|
78
99
|
# @return [Boolean] true if signature is valid, false otherwise
|
79
100
|
# @raise [StandardError] Rescued internally, returns false on any error
|
80
101
|
# @note This method is designed to be safe and will never raise exceptions
|
@@ -91,11 +112,9 @@ module Hooks
|
|
91
112
|
|
92
113
|
validator_config = build_config(config)
|
93
114
|
|
94
|
-
# Security: Check raw headers
|
95
|
-
unless
|
96
|
-
|
97
|
-
return false
|
98
|
-
end
|
115
|
+
# Security: Check raw headers and payload BEFORE processing
|
116
|
+
return false unless valid_headers?(headers)
|
117
|
+
return false unless valid_payload_size?(payload)
|
99
118
|
|
100
119
|
signature_header = validator_config[:header]
|
101
120
|
|
@@ -107,23 +126,37 @@ module Hooks
|
|
107
126
|
return false
|
108
127
|
end
|
109
128
|
|
110
|
-
#
|
111
|
-
|
112
|
-
log.warn("Auth::HMAC validation failed: Signature contains leading/trailing whitespace")
|
113
|
-
return false
|
114
|
-
end
|
115
|
-
|
116
|
-
# Security: Reject signatures containing null bytes or other control characters
|
117
|
-
if raw_signature.match?(/[\u0000-\u001f\u007f-\u009f]/)
|
118
|
-
log.warn("Auth::HMAC validation failed: Signature contains control characters")
|
119
|
-
return false
|
120
|
-
end
|
129
|
+
# Validate signature format using shared validation but with HMAC-specific length limit
|
130
|
+
return false unless validate_signature_format(raw_signature)
|
121
131
|
|
122
132
|
# Now we can safely normalize headers for the rest of the validation
|
123
133
|
normalized_headers = normalize_headers(headers)
|
124
|
-
|
134
|
+
|
135
|
+
# Handle structured headers (e.g., Tailscale format: "t=123,v1=abc")
|
136
|
+
if validator_config[:header_format] == "structured"
|
137
|
+
parsed_signature_data = parse_structured_header(raw_signature, validator_config)
|
138
|
+
if parsed_signature_data.nil?
|
139
|
+
log.warn("Auth::HMAC validation failed: Could not parse structured signature header")
|
140
|
+
return false
|
141
|
+
end
|
142
|
+
|
143
|
+
provided_signature = parsed_signature_data[:signature]
|
144
|
+
|
145
|
+
# For structured headers, timestamp comes from the signature header itself
|
146
|
+
if parsed_signature_data[:timestamp]
|
147
|
+
normalized_headers = normalized_headers.merge(
|
148
|
+
"extracted_timestamp" => parsed_signature_data[:timestamp]
|
149
|
+
)
|
150
|
+
# Override timestamp_header to use our extracted timestamp
|
151
|
+
validator_config = validator_config.merge(timestamp_header: "extracted_timestamp")
|
152
|
+
end
|
153
|
+
else
|
154
|
+
provided_signature = normalized_headers[signature_header.downcase]
|
155
|
+
end
|
125
156
|
|
126
157
|
# Validate timestamp if required (for services that include timestamp validation)
|
158
|
+
# It should be noted that not all HMAC implementations require timestamp validation,
|
159
|
+
# so this is optional based on configuration.
|
127
160
|
if validator_config[:timestamp_header]
|
128
161
|
unless valid_timestamp?(normalized_headers, validator_config)
|
129
162
|
log.warn("Auth::HMAC validation failed: Invalid timestamp")
|
@@ -154,6 +187,22 @@ module Hooks
|
|
154
187
|
|
155
188
|
private
|
156
189
|
|
190
|
+
# Validate signature format for HMAC (uses HMAC-specific length limit)
|
191
|
+
#
|
192
|
+
# @param signature [String] Raw signature to validate
|
193
|
+
# @return [Boolean] true if signature is valid
|
194
|
+
# @api private
|
195
|
+
def self.validate_signature_format(signature)
|
196
|
+
# Check signature length with HMAC-specific limit
|
197
|
+
if signature.length > MAX_SIGNATURE_LENGTH
|
198
|
+
log.warn("Auth::HMAC validation failed: Signature length exceeds maximum limit of #{MAX_SIGNATURE_LENGTH} characters")
|
199
|
+
return false
|
200
|
+
end
|
201
|
+
|
202
|
+
# Use shared validation for other checks
|
203
|
+
valid_header_value?(signature, "Signature")
|
204
|
+
end
|
205
|
+
|
157
206
|
# Build final configuration by merging defaults with provided config
|
158
207
|
#
|
159
208
|
# Combines default configuration values with user-provided settings,
|
@@ -176,7 +225,12 @@ module Hooks
|
|
176
225
|
algorithm: algorithm,
|
177
226
|
format: validator_config[:format] || DEFAULT_CONFIG[:format],
|
178
227
|
version_prefix: validator_config[:version_prefix] || DEFAULT_CONFIG[:version_prefix],
|
179
|
-
payload_template: validator_config[:payload_template]
|
228
|
+
payload_template: validator_config[:payload_template],
|
229
|
+
header_format: validator_config[:header_format] || DEFAULT_CONFIG[:header_format],
|
230
|
+
signature_key: validator_config[:signature_key] || "v1",
|
231
|
+
timestamp_key: validator_config[:timestamp_key] || "t",
|
232
|
+
structured_header_separator: validator_config[:structured_header_separator] || ",",
|
233
|
+
key_value_separator: validator_config[:key_value_separator] || "="
|
180
234
|
})
|
181
235
|
end
|
182
236
|
|
@@ -216,14 +270,6 @@ module Hooks
|
|
216
270
|
timestamp_validator.valid?(timestamp_value, tolerance)
|
217
271
|
end
|
218
272
|
|
219
|
-
# Get timestamp validator instance
|
220
|
-
#
|
221
|
-
# @return [TimestampValidator] Singleton timestamp validator instance
|
222
|
-
# @api private
|
223
|
-
def self.timestamp_validator
|
224
|
-
@timestamp_validator ||= TimestampValidator.new
|
225
|
-
end
|
226
|
-
|
227
273
|
# Compute HMAC signature based on configuration requirements
|
228
274
|
#
|
229
275
|
# Generates the expected HMAC signature for the given payload using the
|
@@ -321,6 +367,44 @@ module Hooks
|
|
321
367
|
"#{config[:algorithm]}=#{hash}"
|
322
368
|
end
|
323
369
|
end
|
370
|
+
|
371
|
+
# Parse structured signature header containing comma-separated key-value pairs
|
372
|
+
#
|
373
|
+
# Parses signature headers like "t=1663781880,v1=0123456789abcdef..." used by
|
374
|
+
# providers like Tailscale that include multiple values in a single header.
|
375
|
+
#
|
376
|
+
# @param header_value [String] Raw signature header value
|
377
|
+
# @param config [Hash<Symbol, Object>] Validator configuration
|
378
|
+
# @return [Hash<Symbol, String>, nil] Parsed data with :signature and :timestamp keys, or nil if parsing fails
|
379
|
+
# @note Returns nil if the header format is invalid or required keys are missing
|
380
|
+
# @api private
|
381
|
+
def self.parse_structured_header(header_value, config)
|
382
|
+
signature_key = config[:signature_key]
|
383
|
+
timestamp_key = config[:timestamp_key]
|
384
|
+
separator = config[:structured_header_separator]
|
385
|
+
key_value_separator = config[:key_value_separator]
|
386
|
+
|
387
|
+
# Parse comma-separated key-value pairs
|
388
|
+
pairs = {}
|
389
|
+
header_value.split(separator).each do |pair|
|
390
|
+
key, value = pair.split(key_value_separator, 2)
|
391
|
+
return nil if key.nil? || value.nil?
|
392
|
+
|
393
|
+
pairs[key.strip] = value.strip
|
394
|
+
end
|
395
|
+
|
396
|
+
# Extract required signature
|
397
|
+
signature = pairs[signature_key]
|
398
|
+
return nil if signature.nil? || signature.empty?
|
399
|
+
|
400
|
+
result = { signature: signature }
|
401
|
+
|
402
|
+
# Extract optional timestamp
|
403
|
+
timestamp = pairs[timestamp_key]
|
404
|
+
result[:timestamp] = timestamp if timestamp && !timestamp.empty?
|
405
|
+
|
406
|
+
result
|
407
|
+
end
|
324
408
|
end
|
325
409
|
end
|
326
410
|
end
|
@@ -61,11 +61,9 @@ module Hooks
|
|
61
61
|
|
62
62
|
validator_config = build_config(config)
|
63
63
|
|
64
|
-
# Security: Check raw headers
|
65
|
-
unless
|
66
|
-
|
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)
|
69
67
|
|
70
68
|
secret_header = validator_config[:header]
|
71
69
|
|
@@ -77,19 +75,13 @@ module Hooks
|
|
77
75
|
return false
|
78
76
|
end
|
79
77
|
|
80
|
-
|
81
|
-
|
82
|
-
|
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")
|
85
81
|
return false
|
86
82
|
end
|
87
83
|
|
88
|
-
|
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
|
93
85
|
|
94
86
|
# Use secure comparison to prevent timing attacks
|
95
87
|
result = Rack::Utils.secure_compare(secret, stripped_secret)
|
@@ -106,12 +98,6 @@ module Hooks
|
|
106
98
|
|
107
99
|
private
|
108
100
|
|
109
|
-
# Short logger accessor for auth module
|
110
|
-
# @return [Hooks::Log] Logger instance
|
111
|
-
def self.log
|
112
|
-
Hooks::Log.instance
|
113
|
-
end
|
114
|
-
|
115
101
|
# Build final configuration by merging defaults with provided config
|
116
102
|
#
|
117
103
|
# Combines default configuration values with user-provided settings,
|
data/lib/hooks/version.rb
CHANGED