Skip to content

RUBY-3860 Cap BSON decode nesting depth at 200#369

Open
comandeo-mongo wants to merge 2 commits intomongodb:masterfrom
comandeo-mongo:RUBY-3860
Open

RUBY-3860 Cap BSON decode nesting depth at 200#369
comandeo-mongo wants to merge 2 commits intomongodb:masterfrom
comandeo-mongo:RUBY-3860

Conversation

@comandeo-mongo
Copy link
Copy Markdown
Contributor

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 SystemStackError or SIGSEGV. The driver decodes server replies and any user-supplied BSON, so this is a remote DoS vector.

Changes

  • BSON::MAX_NESTING_DEPTH = 200 in lib/bson.rb (matches libbson's BSON_MAX_RECURSION and the Go driver's ExtJSON maxNestingDepth).
  • BSON.with_nesting_depth helper that bumps a per-thread counter and raises BSON::Error::BSONDecodeError when the cap is exceeded.
  • C extension (ext/bson/read.c): pvt_read_field, pvt_get_hash_at_depth, pvt_get_array_at_depth thread an int depth and check it on entry.
  • Pure-Ruby Hash/Array (lib/bson/hash.rb, lib/bson/array.rb): parse_hash_from_buffer / parse_array_from_buffer wrap their bodies in BSON.with_nesting_depth. This covers the JRuby code path (the Java extension delegates back to Ruby Hash.from_bson).
  • ExtJSON (lib/bson/ext_json.rb): parse_obj Array branch and parse_hash use the same helper.

Test plan

  • New spec spec/bson/max_nesting_depth_spec.rb covering:
    • C-ext Hash.from_bson at the cap (passes), one over the cap (raises), and a 100k DoS payload (raises, no crash)
    • Nested-array decoding via Hash.from_bson over the cap
    • ExtJSON.parse_obj for hash and array nesting at the cap, one over, and a 50k DoS payload
  • bundle exec rake spec — 7346 examples, 0 failures (35 pre-existing pending)
  • bundle exec rubocop on touched files — clean

Jira: https://jira.mongodb.org/browse/RUBY-3860

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.
@comandeo-mongo comandeo-mongo marked this pull request as ready for review May 5, 2026 18:23
@comandeo-mongo comandeo-mongo requested a review from a team as a code owner May 5, 2026 18:23
@comandeo-mongo comandeo-mongo requested a review from jamis May 5, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants