RUBY-3860 Cap BSON decode nesting depth at 200#369
Open
comandeo-mongo wants to merge 2 commits intomongodb:masterfrom
Open
RUBY-3860 Cap BSON decode nesting depth at 200#369comandeo-mongo wants to merge 2 commits intomongodb:masterfrom
comandeo-mongo wants to merge 2 commits intomongodb:masterfrom
Conversation
Add a hard cap on document/array nesting depth to prevent stack-overflow DoS on adversarial input. A crafted BSON blob (~800 KB at 100k levels) or deeply nested Extended JSON previously crashed MRI/JRuby with SystemStackError or SIGSEGV. Threading: - C ext: pvt_read_field, get_hash, get_array now thread an int depth parameter and raise BSON::Error::BSONDecodeError when it exceeds 200. - Pure Ruby Hash/Array: parse_hash_from_buffer / parse_array_from_buffer bump a per-thread depth counter via BSON.with_nesting_depth (covers the JRuby code path). - ExtJSON: parse_obj (Array branch) and parse_hash use the same helper. The cap of 200 matches libbson's BSON_MAX_RECURSION and the Go driver's ExtJSON parser.
The previous block-yielding `BSON.with_nesting_depth` helper added two JVM frames per Ruby logical level on JRuby (the helper call plus the yielded block frame). At 201 levels of ExtJSON nesting that pushed the JVM thread stack past its default 2 MB and raised java.lang.StackOverflowError before the depth counter could fire. Replaces the helper with a pair of plain method calls (`enter_nesting_depth` + `leave_nesting_depth`) used inline with a `begin/ensure`. Also folds `parse_hash_inner` back into `parse_hash` (renamed to `parse_hash_body` without going through an extra block frame). Net frame savings on the ExtJSON path: ~3 per level, bringing 201 levels back inside the JVM stack budget. For the deliberately-extreme 50 000-level DoS payload tests, accept either BSON::Error::BSONDecodeError or a JVM StackOverflowError as a valid outcome — both mean the process did not crash.
jamis
approved these changes
May 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a hard cap on document/array nesting depth in all three BSON decode paths to prevent stack-overflow DoS on adversarial input.
A crafted BSON blob (~800 KB at 100k nested subdocuments) or deeply nested Extended JSON previously crashed MRI/JRuby with
SystemStackErrororSIGSEGV. The driver decodes server replies and any user-supplied BSON, so this is a remote DoS vector.Changes
BSON::MAX_NESTING_DEPTH = 200inlib/bson.rb(matches libbson'sBSON_MAX_RECURSIONand the Go driver's ExtJSONmaxNestingDepth).BSON.with_nesting_depthhelper that bumps a per-thread counter and raisesBSON::Error::BSONDecodeErrorwhen the cap is exceeded.ext/bson/read.c):pvt_read_field,pvt_get_hash_at_depth,pvt_get_array_at_depththread anint depthand check it on entry.lib/bson/hash.rb,lib/bson/array.rb):parse_hash_from_buffer/parse_array_from_bufferwrap their bodies inBSON.with_nesting_depth. This covers the JRuby code path (the Java extension delegates back to RubyHash.from_bson).lib/bson/ext_json.rb):parse_objArray branch andparse_hashuse the same helper.Test plan
spec/bson/max_nesting_depth_spec.rbcovering:Hash.from_bsonat the cap (passes), one over the cap (raises), and a 100k DoS payload (raises, no crash)Hash.from_bsonover the capExtJSON.parse_objfor hash and array nesting at the cap, one over, and a 50k DoS payloadbundle exec rake spec— 7346 examples, 0 failures (35 pre-existing pending)bundle exec rubocopon touched files — cleanJira: https://jira.mongodb.org/browse/RUBY-3860