@@ -21,13 +21,15 @@ def uuid
21
21
# @return [void]
22
22
# @note Timeout enforcement should be handled at the server level (e.g., Puma)
23
23
def enforce_request_limits ( config )
24
- # Check content length (handle different header formats and sources)
25
- content_length = headers [ "Content-Length" ] || headers [ "CONTENT_LENGTH" ] ||
26
- headers [ "content-length" ] || headers [ "HTTP_CONTENT_LENGTH" ] ||
27
- env [ "CONTENT_LENGTH" ] || env [ "HTTP_CONTENT_LENGTH" ]
24
+ # Optimized content length check - check most common sources first
25
+ content_length = request . content_length if respond_to? ( :request ) && request . respond_to? ( :content_length )
28
26
29
- # Also try to get from request object directly
30
- content_length ||= request . content_length if respond_to? ( :request ) && request . respond_to? ( :content_length )
27
+ content_length ||= headers [ "Content-Length" ] ||
28
+ headers [ "CONTENT_LENGTH" ] ||
29
+ headers [ "content-length" ] ||
30
+ headers [ "HTTP_CONTENT_LENGTH" ] ||
31
+ env [ "CONTENT_LENGTH" ] ||
32
+ env [ "HTTP_CONTENT_LENGTH" ]
31
33
32
34
content_length = content_length &.to_i
33
35
@@ -45,16 +47,21 @@ def enforce_request_limits(config)
45
47
# @param symbolize [Boolean] Whether to symbolize keys in parsed JSON (default: true)
46
48
# @return [Hash, String] Parsed JSON as Hash (optionally symbolized), or raw body if not JSON
47
49
def parse_payload ( raw_body , headers , symbolize : true )
50
+ # Optimized content type check - check most common header first
48
51
content_type = headers [ "Content-Type" ] || headers [ "CONTENT_TYPE" ] || headers [ "content-type" ] || headers [ "HTTP_CONTENT_TYPE" ]
49
52
50
53
# Try to parse as JSON if content type suggests it or if it looks like JSON
51
54
if content_type &.include? ( "application/json" ) || ( raw_body . strip . start_with? ( "{" , "[" ) rescue false )
52
55
begin
53
- parsed_payload = JSON . parse ( raw_body )
56
+ # Security: Limit JSON parsing depth and complexity to prevent JSON bombs
57
+ parsed_payload = safe_json_parse ( raw_body )
54
58
parsed_payload = parsed_payload . transform_keys ( &:to_sym ) if symbolize && parsed_payload . is_a? ( Hash )
55
59
return parsed_payload
56
- rescue JSON ::ParserError
57
- # If JSON parsing fails, return raw body
60
+ rescue JSON ::ParserError , ArgumentError => e
61
+ # If JSON parsing fails or security limits exceeded, return raw body
62
+ if e . message . include? ( "nesting" ) || e . message . include? ( "depth" )
63
+ log . warn ( "JSON parsing limit exceeded: #{ e . message } " )
64
+ end
58
65
end
59
66
end
60
67
@@ -79,6 +86,29 @@ def load_handler(handler_class_name)
79
86
80
87
private
81
88
89
+ # Safely parse JSON
90
+ #
91
+ # @param json_string [String] The JSON string to parse
92
+ # @return [Hash, Array] Parsed JSON object
93
+ # @raise [JSON::ParserError] If JSON is invalid
94
+ # @raise [ArgumentError] If security limits are exceeded
95
+ def safe_json_parse ( json_string )
96
+ # Security limits for JSON parsing
97
+ max_nesting = ENV . fetch ( "JSON_MAX_NESTING" , "20" ) . to_i
98
+
99
+ # Additional size check before parsing
100
+ if json_string . length > ENV . fetch ( "JSON_MAX_SIZE" , "10485760" ) . to_i # 10MB default
101
+ raise ArgumentError , "JSON payload too large for parsing"
102
+ end
103
+
104
+ JSON . parse ( json_string , {
105
+ max_nesting : max_nesting ,
106
+ create_additions : false , # Security: Disable object creation from JSON
107
+ object_class : Hash , # Use plain Hash instead of custom classes
108
+ array_class : Array # Use plain Array instead of custom classes
109
+ } )
110
+ end
111
+
82
112
# Determine HTTP error code from exception
83
113
#
84
114
# @param exception [Exception] The exception to map to an HTTP status code
0 commit comments