Package manager: npm
Affected module: bson-ext
GitHub repo: mongodb-js/bson-ext
Module description:
A BSON parser Node.JS native addon.
BSON is short for Binary JSON and is the binary-encoded serialization of JSON-like documents.
Out-of-bounds read in bson-ext package. Possible risk: leak of sensitive data.
Vulnerability: buffer over-read in bson serialization. During the serialization of specially crafted object it's possible to construct a large buffer that exceeds the initial buffer. The length is not checked so unmanaged bytes will be written to the output bson document.
I've tested it on Ubuntu 22.04.3 LTS, kernel version: 5.15.0-84-generic.
There is a simple PoC in file poc.js.
You could use the provided Dockerfile in order to preserve the environment.
- Build the image
docker build --tag bson-ext-poc .
- Run the image
docker run --rm bson-ext-poc
- Expected behaviour
$ docker run --rm bson-ext-poc
00000000: 0d02 0000 0578 0000 0200 0000 0054 1905 .....x.......T..
00000010: 0000 0000 0000 0000 0000 0000 135a 7f7f .............Z..
00000020: 7f7f 0000 3100 0000 0000 0000 6469 7361 ....1.......disa
00000030: 626c 6564 2d62 792d 6465 6661 756c 742d bled-by-default-
00000040: 7638 2e74 7572 626f 6661 6e00 736f 6e2f v8.turbofan.son/
00000050: 6c69 6200 2100 0000 0000 0000 6465 7674 lib.!.......devt
00000060: 6f6f 6c73 2e74 696d 656c 696e 652c 7638 ools.timeline,v8
00000070: 007f 0000 3100 0000 0000 0000 696e 7465 ....1.......inte
00000080: 726e 616c 2f63 7279 7074 6f2f 6365 7274 rnal/crypto/cert
00000090: 6966 6963 6174 6500 0000 0000 0000 0000 ificate.........
000000a0: 0000 0000 3100 0000 0000 0000 5c59 0000 ....1.......\Y..
000000b0: 0000 0000 347d 6e4f 670f 000e 5030 1505 ....4}nOg...P0..
000000c0: 0000 0000 2030 1505 0000 0000 2904 003b .... 0......)..;
000000d0: 2027 0000 5100 0000 0000 0000 0000 0000 '..Q...........
000000e0: 0000 0000 003a 1d05 0000 0000 0000 0000 .....:..........
000000f0: 0000 0000 0000 0000 0000 0000 607d 1d05 ............`}..
00000100: 0000 0000 1600 0000 0000 0000 1600 0000 ................
00000110: 0000 0000 0027 1505 0000 0000 3069 1c05 .....'......0i..
00000120: 0000 0000 2100 0000 0000 0000 696e 7465 ....!.......inte
00000130: 726e 616c 2f63 7279 7074 6f2f 7369 6700 rnal/crypto/sig.
00000140: 7f7f 0000 1120 0000 0000 0000 706e 0f05 ..... ......pn..
00000150: 0000 0000 0000 0000 0000 0000 0820 0000 ............. ..
00000160: 0000 0000 0000 0000 0000 0000 0100 0000 ................
00000170: 0000 0000 704f 1d05 0000 0000 4a48 6801 ....pO......JHh.
00000180: 0000 0000 0100 0000 0000 0000 0000 0000 ................
00000190: 0000 0000 1050 1d05 0000 0000 0100 0000 .....P..........
000001a0: 0000 0000 0100 0000 0000 0000 0400 0000 ................
000001b0: 0000 0000 3050 1d05 0000 0000 0100 0000 ....0P..........
000001c0: 0100 0000 0100 0000 0100 0000 0100 0000 ................
000001d0: 0000 0000 486e 1c05 0000 0000 006f 1c05 ....Hn.......o..
000001e0: 0000 0000 386f 1c05 0000 0000 706f 1c05 ....8o......po..
000001f0: 0000 0000 386f 1c05 0000 0000 706f 1c05 ....8o......po..
00000200: 0000 0000 2870 1c05 0000 0000 000a ....(p........
Please note that parts of printable text are leaked. Some non-printable bytes (it could be memory addresses) are also leaked.
Actual source code is here: https://github.com/mongodb-js/bson-ext/tree/v4.0.3
The vulnerability is located in file src/bson.cc. The interesting part is BSONSerializer::SerializeValue() function, especially the part with serialization of Binary type:
} else if (NanEquals(Nan::New(bson->BINARY_CLASS_NAME_STR),
constructorString)) {
this->CommitType(typeLocation, BSON_TYPE_BINARY);
uint32_t length = NanTo<uint32_t>(
NanGet(object, Nan::New(bson->BINARY_POSITION_PROPERTY_NAME_STR)));
Local<Object> bufferObj = NanToObject(
NanGet(object, Nan::New(bson->BINARY_BUFFER_PROPERTY_NAME_STR)));
// Add the deprecated 02 type 4 bytes of size to total
if (NanTo<int32_t>(NanGet(
object, Nan::New(bson->BINARY_SUBTYPE_PROPERTY_NAME_STR))) ==
0x02) {
length = length + 4;
}
this->WriteInt32(length);
this->WriteByte(
object,
Nan::New(bson->BINARY_SUBTYPE_PROPERTY_NAME_STR)); // write subtype
// If type 0x02 write the array length aswell
if (NanTo<int32_t>(NanGet(
object, Nan::New(bson->BINARY_SUBTYPE_PROPERTY_NAME_STR))) ==
0x02) {
length = length - 4;
this->WriteInt32(length);
}
// Write the actual data
this->WriteData(node::Buffer::Data(bufferObj), length);
}
The main target is length
variable, it's taken from the input object's field. Then length
is passed to WriteData()
function along with the pointer to the buffer's data.
void WriteData(const char *data, size_t length) {
if ((size_t)((p + length) - destinationBuffer) > MAX_BSON_SIZE)
throw "document is larger than max bson document size of 16MB";
memcpy(p, data, length);
p += length;
}
Let's analyze it. The value (p - destinationBuffer)
is just a size of already written output bytes and length
is our provided value. So the condition is straightforward: the total size (with the written data
) should not exceed MAX_BSON_SIZE
value (17 MiB). So the maximum value of length
is about 16 MiB.
After this there is a simple call to memcpy()
function that just copies the bytes to the output buffer. If the actual length of data
is less than length
value then the memory after the buffer will be copied.
Actually it's a single place where the buffer's size is not checked. I would suggest the same check as used in other places, something like that:
if ((uint32_t)node::Buffer::Length(bufferObj) < length) {
ThrowAllocatedStringException(64, "Invalid length");
}
I read the entire bson-ext code carefully and accidentally found this.