Java driver RawBsonDocument accessors accept malformed zero-length string field

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Minor - P4
    • None
    • Affects Version/s: None
    • Component/s: BSON
    • None
    • None
    • Java Drivers
    • Not Needed
    • None
    • None
    • None
    • None
    • None
    • None

        1. Summary
          The Java driver's `RawBsonDocument` metadata accessors accept a malformed BSON document that contains a string field with declared length `0`, even though canonical BSON document decoding rejects the same bytes as an invalid string. This matters because `size()`, `containsKey(...)`, and `getFirstKey()` can report attacker-controlled malformed data as a real field before value validation has run.
        1. Affected
        1. Root cause
          `RawBsonDocument(byte[], offset, length)` stores caller-supplied BSON bytes after only null, offset, and minimum document-size checks at `bson/src/main/org/bson/RawBsonDocument.java:105`; it does not decode element values before exposing metadata accessors. `size()` walks element headers, increments the count, reads each name, and calls `skipValue()` at `bson/src/main/org/bson/RawBsonDocument.java:211`, while `getFirstKey()` returns the first name immediately after `readName()` at `bson/src/main/org/bson/RawBsonDocument.java:242`, and `containsKey(...)` returns true as soon as the read name matches at `bson/src/main/org/bson/RawBsonDocument.java:254`. On the skip path, `BsonBinaryReader.skipValue()` handles `STRING` by reading a size and skipping that many bytes at `bson/src/main/org/bson/BsonBinaryReader.java:356`; its helper rejects only negative sizes at `bson/src/main/org/bson/BsonBinaryReader.java:379`, so a declared string length of `0` is accepted for skipping. The canonical decode path does validate the value: `BsonDocumentCodec.decode()` appends each field by calling `readValue(...)` at `bson/src/main/org/bson/codecs/BsonDocumentCodec.java:80`, `BsonStringCodec.decode()` reaches `reader.readString()` at `bson/src/main/org/bson/codecs/BsonStringCodec.java:30`, and `ByteBufferBsonInput.readString()` rejects non-positive string sizes at `bson/src/main/org/bson/io/ByteBufferBsonInput.java:124`.
        1. Reproduction
          ```bash
          bash ./poc/run.sh
          ```

      ```text
      TRIGGERED: java RawBsonDocument accessors accepted malformed zero-length string field
      ```

      This line is emitted only when `size()`, `containsKey("a")`, and `getFirstKey()` accept the malformed raw document while full `BsonDocumentCodec` decoding rejects the same bytes with the expected non-positive string-length error. A build or setup failure, or an accepted full decode, does not produce this fingerprint.

        1. Impact
          A remote malicious MongoDB-compatible server peer that supplies BSON documents to a Java driver client can provide malformed BSON bytes that pass `RawBsonDocument` metadata checks as if field `a` exists, while canonical decoding rejects those bytes as invalid. The practical preconditions are that the application receives or constructs a `RawBsonDocument` from peer-controlled bytes before full value decoding, and then uses `size()`, `containsKey(...)`, or `getFirstKey()` as a validation, routing, authorization, or policy predicate. Under those conditions the bug is a data-integrity issue: application logic can make decisions on malformed data that should have been rejected, while the constructor and BSON type/name guards are satisfied and the full string-value validation is bypassed.
        1. Suggested fix
          ```001-fix.diff
          diff --git a/bson/src/main/org/bson/BsonBinaryReader.java b/bson/src/main/org/bson/BsonBinaryReader.java
          index 7f2a2a1..ce20a7b 100644
          • a/bson/src/main/org/bson/BsonBinaryReader.java
            +++ b/bson/src/main/org/bson/BsonBinaryReader.java
            @@ -354,10 +354,10 @@ public class BsonBinaryReader extends AbstractBsonReader {
            skip = 0;
            break;
            case STRING:
      • skip = readSize();
        + skip = readStringSize();
        break;
        case SYMBOL:
      • skip = readSize();
        + skip = readStringSize();
        break;
        case TIMESTAMP:
        skip = 8;
        @@ -366,7 +366,7 @@ public class BsonBinaryReader extends AbstractBsonReader {
        skip = 0;
        break;
        case DB_POINTER:
      • skip = readSize() + 12; // String followed by ObjectId
        + skip = readStringSize() + 12; // String followed by ObjectId
        break;
        default:
        throw new BSONException("Unexpected BSON type: " + getCurrentBsonType());
        @@ -385,6 +385,15 @@ public class BsonBinaryReader extends AbstractBsonReader { return size; }

      + private int readStringSize() {
      + int size = readSize();
      + if (size <= 0)

      { + throw new BsonSerializationException(format("While decoding a BSON string found a size that is not a positive number: %d", + size)); + }

      + return size;
      + }
      +
      protected Context getContext() {
      return (Context) super.getContext();
      }
      diff --git a/bson/src/main/org/bson/RawBsonDocument.java b/bson/src/main/org/bson/RawBsonDocument.java
      index 9fc60e3..963997e 100644
      — a/bson/src/main/org/bson/RawBsonDocument.java
      +++ b/bson/src/main/org/bson/RawBsonDocument.java
      @@ -243,7 +243,9 @@ public class RawBsonDocument extends BsonDocument {
      try (BsonBinaryReader bsonReader = createReader()) {
      bsonReader.readStartDocument();
      try

      { - return bsonReader.readName(); + String name = bsonReader.readName(); + bsonReader.skipValue(); + return name; }

      catch (BsonInvalidOperationException e) {
      throw new NoSuchElementException();
      }
      @@ -259,9 +261,10 @@ public class RawBsonDocument extends BsonDocument {
      try (BsonBinaryReader bsonReader = createReader()) {
      bsonReader.readStartDocument();
      while (bsonReader.readBsonType() != BsonType.END_OF_DOCUMENT) {

      • if (bsonReader.readName().equals(key)) {
        + String name = bsonReader.readName();
        + bsonReader.skipValue();
        + if (name.equals(key)) { return true; }
      • bsonReader.skipValue();
        }
        bsonReader.readEndDocument();
        }
        ```
        1. References

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

              Created:
              Updated: