js-bson: three input-validation gaps in parsing/serialization

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Minor - P4
    • None
    • Affects Version/s: None
    • Component/s: None
    • 2
    • None
    • None
    • None
    • None
    • None
    • None
    • None

        1. Summary
           
          While auditing `mongodb/js-bson` we found three independent input-validation
          issues that all reduce to the same root pattern: a BSON invariant that is
          enforced on one code path is missing on a neighbouring one. None of them is a
          memory-safety problem — js-bson stays within JS/`Uint8Array` bounds throughout —
          but each lets malformed or oversized input reach an outcome the library is
          otherwise designed to reject (a non-terminating scan, a malformed serialized
          vector, or stack exhaustion).
           
          We are reporting them together because they share a pinned ref, can be fixed in
          one pass, and illustrate the same "validation enforced here but not there" gap.
           
          We want to be upfront about reachability: these are library-level defects with
          real triggers, but exploitability depends on how an embedding application uses
          the affected API. The per-issue "Conditions required" notes below state what an
          application must be doing for the defect to matter; we are not claiming every
          js-bson consumer is exposed.
           
        2. Affected
           
      • Project: `mongodb/js-bson`
      • Pinned ref: `5b42c5a1535d45ec89ab9f1ed3bb249d09730e3c`
         

         
        1. 1. `onDemand.parseToElements` — non-terminating scan on zero-length string
           
      • *What*: A 10-byte BSON document with a string element declaring length `0`
          passes the on-demand parser's size/terminator guards, advances the cursor to
          the declared document end, and then makes `findNull` scan from past the
          buffer. `findNull` has no bounds check, and out-of-range `Uint8Array` reads
          return `undefined` (`undefined !== 0x00` is always true), so the loop never
          terminates — synchronous CPU hang.
      • *Root cause*: `findNull` in `src/parser/on_demand/parse_to_elements.ts`
          assumes the document is well-formed and omits a `< bytes.length` guard. The
          ordinary deserializer rejects `stringSize <= 0` (`src/parser/deserializer.ts`),
          but that check is not mirrored on the on-demand path.
      • *Conditions required: an application must call the **experimental*
          `BSON.onDemand.parseToElements` API directly on untrusted bytes, without a
          surrounding timeout / worker isolation. Applications using the standard
          `BSON.deserialize` path are *not* affected. We acknowledge `onDemand` is not
          a common entry point today.
      • *Details & PoC*: `int-mongodb-js-bson-ondemand-zero-length-string-hang/README.md`
          and `int-mongodb-js-bson-ondemand-zero-length-string-hang/poc/` (run `bash poc/run.sh`).
         
        1. 2. `validateBinaryVector` — zero-length subtype-9 Binary serialized as malformed vector
           
      • *What*: `new Binary(Buffer.alloc(0), Binary.SUBTYPE_VECTOR)` produces a
          subtype-9 (Vector) value with no dtype/padding metadata, yet both
          `BSON.serialize` and `EJSON.stringify` emit it without complaint. A one-byte
          vector is correctly rejected, so this is specifically a missing
          minimum-length invariant, not a fully disabled validator.
      • *Root cause*: `validateBinaryVector` in `src/binary.ts` reads
          `vector.buffer[0]` (dtype) and `vector.buffer[1]` (padding) without first
          checking that those two bytes exist. With a zero-length payload both reads are
          `undefined`, so every dtype-specific branch is skipped and validation passes.
          The typed helper constructors (`fromInt8Array`, etc.) always allocate
          `byteLength + 2`, so the invariant is "every subtype-9 value carries 2 metadata
          bytes" — just not enforced on the raw constructor path.
      • *Conditions required*: an application must wrap caller-controlled bytes in
          the raw `Binary(buf, SUBTYPE_VECTOR)` constructor (rather than the typed
          helpers) and then treat js-bson serialization as its validation boundary
          before persisting/exporting. The impact is integrity (malformed vector data
          emitted), not memory safety or a server-side acceptance claim.
      • *Details & PoC*: `int-mongodb-js-bson-empty-vector-serialization-bypass/README.md`
          and `int-mongodb-js-bson-empty-vector-serialization-bypass/poc/` (run `bash poc/run.sh`).
         
        1. 3. `BSON.serialize` — stack exhaustion on deeply nested documents
           
      • *What: A deeply nested *acyclic plain-object document
          (`{a:{a:{...{leaf:1}}}}`) drives unbounded recursion in serialization until V8
          throws `RangeError: Maximum call stack size exceeded`. The existing
          circular-reference guard does not catch this because each level is a fresh
          object.
      • *Root cause*: `serializeObject` → `serializeInto` in
          `src/parser/serializer.ts` threads a `depth` argument but never compares it
          against a limit. There is no `BSON_MAX_NESTING_DEPTH` enforcement, unlike the
          MongoDB server's own document nesting cap.
      • *Conditions required*: an application must serialize request-derived
          documents (e.g. a JSON request body) with `BSON.serialize` /
          `serializeWithBufferAndIndex` *without its own depth guard first*. This is
          the most realistic of the three: standard JSON body parsers enforce size but
          not depth, so a small (~1 KB) but deeply nested payload reaches the encoder.
          Practical impact still depends on the app's error handling — a caught
          `RangeError` degrades to a failed request rather than a process kill in most
          setups.
      • *Details & PoC*: `int-mongodb-js-bson-serialize-depth-dos/README.md`
          and `int-mongodb-js-bson-serialize-depth-dos/poc/` (run `bash poc/run.sh`).
         

         
        1. Suggested direction
           
          All three are addressable in a single change set:
           
          1. Add a `< bytes.length` bound to `findNull` (and treat the overrun as the
             existing "null terminator not found" error).
          2. Reject subtype-9 `Binary` with fewer than 2 bytes in `validateBinaryVector`.
          3. Introduce a `BSON_MAX_NESTING_DEPTH` limit (the server uses 100) checked in
             the serialize/size paths.
           
          Concrete diffs are included in the per-issue `README.md` files referenced above.
           
        2. Reproduction
           
          Each PoC is self-contained: it clones js-bson at the pinned ref, builds against
          that exact source, and prints a `TRIGGERED:` fingerprint only on the vulnerable
          behaviour. See each issue's `poc/run.sh`.
           

            Assignee:
            Sean Milligan
            Reporter:
            Youngjoon Kim
            None
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated: