Skip to content

Commit f99fdfd

Browse files
authored
fix(NODE-6536): Binary.read never returns number[] and reads beyond content (#727)
1 parent 887849d commit f99fdfd

File tree

4 files changed

+108
-55
lines changed

4 files changed

+108
-55
lines changed

src/binary.ts

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,48 @@ export class Binary extends BSONValue {
6161
/** User BSON type */
6262
static readonly SUBTYPE_USER_DEFINED = 128;
6363

64-
buffer!: Uint8Array;
65-
sub_type!: number;
66-
position!: number;
64+
/**
65+
* The bytes of the Binary value.
66+
*
67+
* The format of a Binary value in BSON is defined as:
68+
* ```txt
69+
* binary ::= int32 subtype (byte*)
70+
* ```
71+
*
72+
* This `buffer` is the "(byte*)" segment.
73+
*
74+
* Unless the value is subtype 2, then deserialize will read the first 4 bytes as an int32 and set this to the remaining bytes.
75+
*
76+
* ```txt
77+
* binary ::= int32 unsigned_byte(2) int32 (byte*)
78+
* ```
79+
*
80+
* @see https://bsonspec.org/spec.html
81+
*/
82+
public buffer: Uint8Array;
83+
/**
84+
* The binary subtype.
85+
*
86+
* Current defined values are:
87+
*
88+
* - `unsigned_byte(0)` Generic binary subtype
89+
* - `unsigned_byte(1)` Function
90+
* - `unsigned_byte(2)` Binary (Deprecated)
91+
* - `unsigned_byte(3)` UUID (Deprecated)
92+
* - `unsigned_byte(4)` UUID
93+
* - `unsigned_byte(5)` MD5
94+
* - `unsigned_byte(6)` Encrypted BSON value
95+
* - `unsigned_byte(7)` Compressed BSON column
96+
* - `unsigned_byte(8)` Sensitive
97+
* - `unsigned_byte(9)` Vector
98+
* - `unsigned_byte(128)` - `unsigned_byte(255)` User defined
99+
*/
100+
public sub_type: number;
101+
/**
102+
* The Binary's `buffer` can be larger than the Binary's content.
103+
* This property is used to determine where the content ends in the buffer.
104+
*/
105+
public position: number;
67106

68107
/**
69108
* Create a new Binary instance.
@@ -160,16 +199,15 @@ export class Binary extends BSONValue {
160199
}
161200

162201
/**
163-
* Reads **length** bytes starting at **position**.
202+
* Returns a view of **length** bytes starting at **position**.
164203
*
165204
* @param position - read from the given position in the Binary.
166205
* @param length - the number of bytes to read.
167206
*/
168-
read(position: number, length: number): BinarySequence {
207+
read(position: number, length: number): Uint8Array {
169208
length = length && length > 0 ? length : this.position;
170-
171-
// Let's return the data based on the type we have
172-
return this.buffer.slice(position, position + length);
209+
const end = position + length;
210+
return this.buffer.subarray(position, end > this.position ? this.position : end);
173211
}
174212

175213
/** returns a view of the binary value as a Uint8Array */
@@ -219,7 +257,7 @@ export class Binary extends BSONValue {
219257

220258
toUUID(): UUID {
221259
if (this.sub_type === Binary.SUBTYPE_UUID) {
222-
return new UUID(this.buffer.slice(0, this.position));
260+
return new UUID(this.buffer.subarray(0, this.position));
223261
}
224262

225263
throw new BSONError(

src/parser/deserializer.ts

Lines changed: 17 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ function deserializeObject(
296296

297297
// We have a raw value
298298
if (raw) {
299-
value = buffer.slice(index, index + objectSize);
299+
value = buffer.subarray(index, index + objectSize);
300300
} else {
301301
let objectOptions = options;
302302
if (!globalUTFValidation) {
@@ -374,52 +374,24 @@ function deserializeObject(
374374
if (binarySize > buffer.byteLength)
375375
throw new BSONError('Binary type size larger than document size');
376376

377-
// Decode as raw Buffer object if options specifies it
378-
if (buffer['slice'] != null) {
379-
// If we have subtype 2 skip the 4 bytes for the size
380-
if (subType === Binary.SUBTYPE_BYTE_ARRAY) {
381-
binarySize = NumberUtils.getInt32LE(buffer, index);
382-
index += 4;
383-
if (binarySize < 0)
384-
throw new BSONError('Negative binary type element size found for subtype 0x02');
385-
if (binarySize > totalBinarySize - 4)
386-
throw new BSONError('Binary type with subtype 0x02 contains too long binary size');
387-
if (binarySize < totalBinarySize - 4)
388-
throw new BSONError('Binary type with subtype 0x02 contains too short binary size');
389-
}
377+
// If we have subtype 2 skip the 4 bytes for the size
378+
if (subType === Binary.SUBTYPE_BYTE_ARRAY) {
379+
binarySize = NumberUtils.getInt32LE(buffer, index);
380+
index += 4;
381+
if (binarySize < 0)
382+
throw new BSONError('Negative binary type element size found for subtype 0x02');
383+
if (binarySize > totalBinarySize - 4)
384+
throw new BSONError('Binary type with subtype 0x02 contains too long binary size');
385+
if (binarySize < totalBinarySize - 4)
386+
throw new BSONError('Binary type with subtype 0x02 contains too short binary size');
387+
}
390388

391-
if (promoteBuffers && promoteValues) {
392-
value = ByteUtils.toLocalBufferType(buffer.slice(index, index + binarySize));
393-
} else {
394-
value = new Binary(buffer.slice(index, index + binarySize), subType);
395-
if (subType === constants.BSON_BINARY_SUBTYPE_UUID_NEW && UUID.isValid(value)) {
396-
value = value.toUUID();
397-
}
398-
}
389+
if (promoteBuffers && promoteValues) {
390+
value = ByteUtils.toLocalBufferType(buffer.subarray(index, index + binarySize));
399391
} else {
400-
// If we have subtype 2 skip the 4 bytes for the size
401-
if (subType === Binary.SUBTYPE_BYTE_ARRAY) {
402-
binarySize = NumberUtils.getInt32LE(buffer, index);
403-
index += 4;
404-
if (binarySize < 0)
405-
throw new BSONError('Negative binary type element size found for subtype 0x02');
406-
if (binarySize > totalBinarySize - 4)
407-
throw new BSONError('Binary type with subtype 0x02 contains too long binary size');
408-
if (binarySize < totalBinarySize - 4)
409-
throw new BSONError('Binary type with subtype 0x02 contains too short binary size');
410-
}
411-
412-
if (promoteBuffers && promoteValues) {
413-
value = ByteUtils.allocateUnsafe(binarySize);
414-
// Copy the data
415-
for (i = 0; i < binarySize; i++) {
416-
value[i] = buffer[index + i];
417-
}
418-
} else {
419-
value = new Binary(buffer.slice(index, index + binarySize), subType);
420-
if (subType === constants.BSON_BINARY_SUBTYPE_UUID_NEW && UUID.isValid(value)) {
421-
value = value.toUUID();
422-
}
392+
value = new Binary(buffer.subarray(index, index + binarySize), subType);
393+
if (subType === constants.BSON_BINARY_SUBTYPE_UUID_NEW && UUID.isValid(value)) {
394+
value = value.toUUID();
423395
}
424396
}
425397

test/node/binary.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from 'chai';
22
import * as vm from 'node:vm';
3-
import { Binary, BSON } from '../register-bson';
3+
import { __isWeb__, Binary, BSON } from '../register-bson';
44
import * as util from 'node:util';
55

66
describe('class Binary', () => {
@@ -81,6 +81,46 @@ describe('class Binary', () => {
8181
});
8282
});
8383

84+
describe('read()', function () {
85+
const LocalBuffer = __isWeb__ ? Uint8Array : Buffer;
86+
87+
it('reads a single byte from the buffer', function () {
88+
const binary = new Binary();
89+
binary.put(0x42);
90+
expect(binary.read(0, 1)).to.deep.equal(LocalBuffer.of(0x42));
91+
});
92+
93+
it('does not read beyond binary.position', function () {
94+
const binary = new Binary();
95+
binary.put(0x42);
96+
expect(binary.buffer.byteLength).to.equal(256);
97+
expect(binary.read(0, 10)).to.deep.equal(LocalBuffer.of(0x42));
98+
});
99+
100+
it('reads a single byte from the buffer at the given position', function () {
101+
const binary = new Binary();
102+
binary.put(0x42);
103+
binary.put(0x43);
104+
binary.put(0x44);
105+
expect(binary.read(1, 1)).to.deep.equal(LocalBuffer.of(0x43));
106+
});
107+
108+
it('reads nothing if the position is out of bounds', function () {
109+
const binary = new Binary();
110+
expect(binary.read(1, 0)).to.have.lengthOf(0);
111+
});
112+
113+
it('sets length to position if not provided', function () {
114+
const binary = new Binary();
115+
binary.put(0x42);
116+
binary.put(0x42);
117+
binary.put(0x42);
118+
expect(binary.position).to.equal(3);
119+
// @ts-expect-error: checking behavior TS doesn't support
120+
expect(binary.read(0)).to.have.lengthOf(3);
121+
});
122+
});
123+
84124
context('inspect()', () => {
85125
it(`when value is default returns "Binary.createFromBase64('', 0)"`, () => {
86126
expect(util.inspect(new Binary())).to.equal(`Binary.createFromBase64('', 0)`);
@@ -93,6 +133,7 @@ describe('class Binary', () => {
93133
});
94134

95135
it(`when value is default with a subtype returns "Binary.createFromBase64('', 35)"`, () => {
136+
// @ts-expect-error: check null is handled the same as undefined
96137
expect(util.inspect(new Binary(null, 0x23))).to.equal(`Binary.createFromBase64('', 35)`);
97138
});
98139

test/types/bson.test-d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,5 @@ expectNotDeprecated(new ObjectId(42 as string | number));
8585

8686
// Timestamp accepts timestamp because constructor allows: {i:number, t:number}
8787
new Timestamp(new Timestamp(0n))
88+
89+
expectType<(position: number, length: number) => Uint8Array>(Binary.prototype.read);

0 commit comments

Comments
 (0)