Skip to content

Commit e179614

Browse files
committed
Unbounded read in Rack::Request form parsing can lead to memory exhaustion.
- Limit read to `query_parser.bytesize_limit`.
1 parent 57277b7 commit e179614

File tree

4 files changed

+86
-2
lines changed

4 files changed

+86
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. For info on
77
### Security
88

99
- [CVE-2025-61780](https://github.com/advisories/GHSA-r657-rxjc-j557) Improper handling of headers in `Rack::Sendfile` may allow proxy bypass.
10+
- [CVE-2025-61919](https://github.com/advisories/GHSA-6xw4-3v39-52mm) Unbounded read in `Rack::Request` form parsing can lead to memory exhaustion.
1011

1112
## [3.2.2] - 2025-10-07
1213

lib/rack/query_parser.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ def self.make_default(param_depth_limit, **options)
5757
PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
5858
private_constant :PARAMS_LIMIT
5959

60+
attr_reader :bytesize_limit
61+
6062
def initialize(params_class, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
6163
@params_class = params_class
6264
@param_depth_limit = param_depth_limit
@@ -221,7 +223,7 @@ def each_query_pair(qs, separator, unescaper = nil)
221223
return if !qs || qs.empty?
222224

223225
if qs.bytesize > @bytesize_limit
224-
raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
226+
raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
225227
end
226228

227229
pairs = qs.split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP, @params_limit + 1)

lib/rack/request.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,10 @@ def form_pairs
513513
if pairs = Rack::Multipart.parse_multipart(env, Rack::Multipart::ParamList)
514514
set_header RACK_REQUEST_FORM_PAIRS, pairs
515515
else
516-
form_vars = get_header(RACK_INPUT).read
516+
# Add 2 bytes. One to check whether it is over the limit, and a second
517+
# in case the slice! call below removes the last byte
518+
# If read returns nil, use the empty string
519+
form_vars = get_header(RACK_INPUT).read(query_parser.bytesize_limit + 2) || ''
517520

518521
# Fix for Safari Ajax postings that always append \0
519522
# form_vars.sub!(/\0\z/, '') # performance replacement:

test/spec_request.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,84 @@ def initialize(*)
795795
req.POST.must_equal "foo" => "bar", "quux" => "bla"
796796
end
797797

798+
it "limit POST body read to bytesize_limit when parsing url-encoded data" do
799+
# Create a mock input that tracks read calls
800+
reads = []
801+
mock_input = Object.new
802+
mock_input.define_singleton_method(:read) do |len=nil|
803+
reads << len
804+
# Return mutable string
805+
"foo=bar".dup
806+
end
807+
808+
request = make_request \
809+
Rack::MockRequest.env_for("/",
810+
'REQUEST_METHOD' => 'POST',
811+
'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
812+
'rack.input' => mock_input)
813+
814+
request.POST.must_equal "foo" => "bar"
815+
816+
# Verify read was called with a limit (bytesize_limit + 2), not nil
817+
reads.size.must_equal 1
818+
reads.first.wont_be_nil
819+
reads.first.must_equal(request.send(:query_parser).bytesize_limit + 2)
820+
end
821+
822+
it "handle nil return from rack.input.read when parsing url-encoded data" do
823+
# Simulate an input that returns nil on read
824+
mock_input = Object.new
825+
mock_input.define_singleton_method(:read) do |len=nil|
826+
nil
827+
end
828+
829+
request = make_request \
830+
Rack::MockRequest.env_for("/",
831+
'REQUEST_METHOD' => 'POST',
832+
'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
833+
'rack.input' => mock_input)
834+
835+
# Should handle nil gracefully and return empty hash
836+
request.POST.must_equal({})
837+
end
838+
839+
it "truncate POST body at bytesize_limit when parsing url-encoded data" do
840+
# Create input larger than limit
841+
large_body = "a=1&" * 1000000 # Very large body
842+
843+
request = make_request \
844+
Rack::MockRequest.env_for("/",
845+
'REQUEST_METHOD' => 'POST',
846+
'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
847+
:input => large_body)
848+
849+
# Should parse only up to the limit without reading entire body into memory
850+
# The actual parsing may fail due to size limit, which is expected
851+
proc { request.POST }.must_raise Rack::QueryParser::QueryLimitError
852+
end
853+
854+
it "clean up Safari's ajax POST body with limited read" do
855+
# Verify Safari null-byte cleanup still works with bounded read
856+
reads = []
857+
mock_input = Object.new
858+
mock_input.define_singleton_method(:read) do |len=nil|
859+
reads << len
860+
# Return mutable string (dup ensures it's not frozen)
861+
"foo=bar\0".dup
862+
end
863+
864+
request = make_request \
865+
Rack::MockRequest.env_for("/",
866+
'REQUEST_METHOD' => 'POST',
867+
'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
868+
'rack.input' => mock_input)
869+
870+
request.POST.must_equal "foo" => "bar"
871+
872+
# Verify bounded read was used
873+
reads.first.wont_be_nil
874+
end
875+
798876
it "return form_pairs for url-encoded POST data" do
799877
req = make_request \
800878
Rack::MockRequest.env_for("/",

0 commit comments

Comments
 (0)