@@ -21,13 +21,15 @@ def uuid
2121 # @return [void]
2222 # @note Timeout enforcement should be handled at the server level (e.g., Puma)
2323 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 )
2826
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" ]
3133
3234 content_length = content_length &.to_i
3335
@@ -45,16 +47,21 @@ def enforce_request_limits(config)
4547 # @param symbolize [Boolean] Whether to symbolize keys in parsed JSON (default: true)
4648 # @return [Hash, String] Parsed JSON as Hash (optionally symbolized), or raw body if not JSON
4749 def parse_payload ( raw_body , headers , symbolize : true )
50+ # Optimized content type check - check most common header first
4851 content_type = headers [ "Content-Type" ] || headers [ "CONTENT_TYPE" ] || headers [ "content-type" ] || headers [ "HTTP_CONTENT_TYPE" ]
4952
5053 # Try to parse as JSON if content type suggests it or if it looks like JSON
5154 if content_type &.include? ( "application/json" ) || ( raw_body . strip . start_with? ( "{" , "[" ) rescue false )
5255 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 )
5458 parsed_payload = parsed_payload . transform_keys ( &:to_sym ) if symbolize && parsed_payload . is_a? ( Hash )
5559 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
5865 end
5966 end
6067
@@ -79,6 +86,29 @@ def load_handler(handler_class_name)
7986
8087 private
8188
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+
82112 # Determine HTTP error code from exception
83113 #
84114 # @param exception [Exception] The exception to map to an HTTP status code
0 commit comments