Skip to content

Commit d239cd1

Browse files
authored
fix(NODE-4960): UUID validation too strict (#572)
1 parent 91b84ed commit d239cd1

File tree

5 files changed

+154
-131
lines changed

5 files changed

+154
-131
lines changed

src/binary.ts

+47-40
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { bufferToUuidHexString, uuidHexStringToBuffer, uuidValidateString } from './uuid_utils';
21
import { isUint8Array } from './parser/utils';
32
import type { EJSONOptions } from './extended_json';
43
import { BSONError } from './error';
@@ -288,7 +287,7 @@ export class Binary extends BSONValue {
288287
}
289288
} else if ('$uuid' in doc) {
290289
type = 4;
291-
data = uuidHexStringToBuffer(doc.$uuid);
290+
data = UUID.bytesFromString(doc.$uuid);
292291
}
293292
if (!data) {
294293
throw new BSONError(`Unexpected Binary Extended JSON format ${JSON.stringify(doc)}`);
@@ -311,42 +310,40 @@ export class Binary extends BSONValue {
311310
export type UUIDExtended = {
312311
$uuid: string;
313312
};
313+
314314
const UUID_BYTE_LENGTH = 16;
315+
const UUID_WITHOUT_DASHES = /^[0-9A-F]{32}$/i;
316+
const UUID_WITH_DASHES = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
315317

316318
/**
317319
* A class representation of the BSON UUID type.
318320
* @public
319321
*/
320322
export class UUID extends Binary {
321-
static cacheHexString: boolean;
322-
323-
/** UUID hexString cache @internal */
324-
private __id?: string;
325-
323+
static cacheHexString = false;
326324
/**
327-
* Create an UUID type
325+
* Create a UUID type
326+
*
327+
* When the argument to the constructor is omitted a random v4 UUID will be generated.
328328
*
329329
* @param input - Can be a 32 or 36 character hex string (dashes excluded/included) or a 16 byte binary Buffer.
330330
*/
331331
constructor(input?: string | Uint8Array | UUID) {
332332
let bytes: Uint8Array;
333-
let hexStr;
334333
if (input == null) {
335334
bytes = UUID.generate();
336335
} else if (input instanceof UUID) {
337336
bytes = ByteUtils.toLocalBufferType(new Uint8Array(input.buffer));
338-
hexStr = input.__id;
339337
} else if (ArrayBuffer.isView(input) && input.byteLength === UUID_BYTE_LENGTH) {
340338
bytes = ByteUtils.toLocalBufferType(input);
341339
} else if (typeof input === 'string') {
342-
bytes = uuidHexStringToBuffer(input);
340+
bytes = UUID.bytesFromString(input);
343341
} else {
344342
throw new BSONError(
345343
'Argument passed in UUID constructor must be a UUID, a 16 byte Buffer or a 32/36 character hex string (dashes excluded/included, format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).'
346344
);
347345
}
348346
super(bytes, BSON_BINARY_SUBTYPE_UUID_NEW);
349-
this.__id = hexStr;
350347
}
351348

352349
/**
@@ -359,28 +356,23 @@ export class UUID extends Binary {
359356

360357
set id(value: Uint8Array) {
361358
this.buffer = value;
362-
363-
if (UUID.cacheHexString) {
364-
this.__id = bufferToUuidHexString(value);
365-
}
366359
}
367360

368361
/**
369362
* Returns the UUID id as a 32 or 36 character hex string representation, excluding/including dashes (defaults to 36 character dash separated)
370363
* @param includeDashes - should the string exclude dash-separators.
371364
* */
372365
toHexString(includeDashes = true): string {
373-
if (UUID.cacheHexString && this.__id) {
374-
return this.__id;
375-
}
376-
377-
const uuidHexString = bufferToUuidHexString(this.id, includeDashes);
378-
379-
if (UUID.cacheHexString) {
380-
this.__id = uuidHexString;
366+
if (includeDashes) {
367+
return [
368+
ByteUtils.toHex(this.buffer.subarray(0, 4)),
369+
ByteUtils.toHex(this.buffer.subarray(4, 6)),
370+
ByteUtils.toHex(this.buffer.subarray(6, 8)),
371+
ByteUtils.toHex(this.buffer.subarray(8, 10)),
372+
ByteUtils.toHex(this.buffer.subarray(10, 16))
373+
].join('-');
381374
}
382-
383-
return uuidHexString;
375+
return ByteUtils.toHex(this.buffer);
384376
}
385377

386378
/**
@@ -446,37 +438,32 @@ export class UUID extends Binary {
446438
* Checks if a value is a valid bson UUID
447439
* @param input - UUID, string or Buffer to validate.
448440
*/
449-
static isValid(input: string | Uint8Array | UUID): boolean {
441+
static isValid(input: string | Uint8Array | UUID | Binary): boolean {
450442
if (!input) {
451443
return false;
452444
}
453445

454-
if (input instanceof UUID) {
455-
return true;
456-
}
457-
458446
if (typeof input === 'string') {
459-
return uuidValidateString(input);
447+
return UUID.isValidUUIDString(input);
460448
}
461449

462450
if (isUint8Array(input)) {
463-
// check for length & uuid version (https://tools.ietf.org/html/rfc4122#section-4.1.3)
464-
if (input.byteLength !== UUID_BYTE_LENGTH) {
465-
return false;
466-
}
467-
468-
return (input[6] & 0xf0) === 0x40 && (input[8] & 0x80) === 0x80;
451+
return input.byteLength === UUID_BYTE_LENGTH;
469452
}
470453

471-
return false;
454+
return (
455+
input._bsontype === 'Binary' &&
456+
input.sub_type === this.SUBTYPE_UUID &&
457+
input.buffer.byteLength === 16
458+
);
472459
}
473460

474461
/**
475462
* Creates an UUID from a hex string representation of an UUID.
476463
* @param hexString - 32 or 36 character hex string (dashes excluded/included).
477464
*/
478465
static override createFromHexString(hexString: string): UUID {
479-
const buffer = uuidHexStringToBuffer(hexString);
466+
const buffer = UUID.bytesFromString(hexString);
480467
return new UUID(buffer);
481468
}
482469

@@ -485,6 +472,26 @@ export class UUID extends Binary {
485472
return new UUID(ByteUtils.fromBase64(base64));
486473
}
487474

475+
/** @internal */
476+
static bytesFromString(representation: string) {
477+
if (!UUID.isValidUUIDString(representation)) {
478+
throw new BSONError(
479+
'UUID string representation must be 32 hex digits or canonical hyphenated representation'
480+
);
481+
}
482+
return ByteUtils.fromHex(representation.replace(/-/g, ''));
483+
}
484+
485+
/**
486+
* @internal
487+
*
488+
* Validates a string to be a hex digit sequence with or without dashes.
489+
* The canonical hyphenated representation of a uuid is hex in 8-4-4-4-12 groups.
490+
*/
491+
static isValidUUIDString(representation: string) {
492+
return UUID_WITHOUT_DASHES.test(representation) || UUID_WITH_DASHES.test(representation);
493+
}
494+
488495
/**
489496
* Converts to a string representation of this Id.
490497
*

src/parser/deserializer.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Binary } from '../binary';
2-
import type { Document } from '../bson';
2+
import { Document, UUID } from '../bson';
33
import { Code } from '../code';
44
import * as constants from '../constants';
55
import { DBRef, DBRefLike, isDBRefLike } from '../db_ref';
@@ -404,7 +404,7 @@ function deserializeObject(
404404
value = ByteUtils.toLocalBufferType(buffer.slice(index, index + binarySize));
405405
} else {
406406
value = new Binary(buffer.slice(index, index + binarySize), subType);
407-
if (subType === constants.BSON_BINARY_SUBTYPE_UUID_NEW) {
407+
if (subType === constants.BSON_BINARY_SUBTYPE_UUID_NEW && UUID.isValid(value)) {
408408
value = value.toUUID();
409409
}
410410
}
@@ -432,10 +432,11 @@ function deserializeObject(
432432

433433
if (promoteBuffers && promoteValues) {
434434
value = _buffer;
435-
} else if (subType === constants.BSON_BINARY_SUBTYPE_UUID_NEW) {
436-
value = new Binary(buffer.slice(index, index + binarySize), subType).toUUID();
437435
} else {
438436
value = new Binary(buffer.slice(index, index + binarySize), subType);
437+
if (subType === constants.BSON_BINARY_SUBTYPE_UUID_NEW && UUID.isValid(value)) {
438+
value = value.toUUID();
439+
}
439440
}
440441
}
441442

src/uuid_utils.ts

-33
This file was deleted.

test/node/tools/utils.js

+28
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,34 @@ const bufferFromHexArray = array => {
128128

129129
exports.bufferFromHexArray = bufferFromHexArray;
130130

131+
/**
132+
* A companion helper to bufferFromHexArray to help with constructing bson bytes manually.
133+
* When creating a BSON Binary you need a leading little endian int32 followed by a sequence of bytes
134+
* of that length.
135+
*
136+
* @example
137+
* ```js
138+
* const binAsHex = '000000';
139+
* const serializedUUID = bufferFromHexArray([
140+
* '05', // binData type
141+
* '6100', // 'a' & null
142+
* int32ToHex(binAsHex.length / 2), // binary starts with int32 length
143+
* '7F', // user subtype
144+
* binAsHex // uuid bytes
145+
* ]);
146+
* ```
147+
*
148+
* @param {number | Int32} int32 -
149+
* @returns
150+
*/
151+
function int32LEToHex(int32) {
152+
const buf = Buffer.alloc(4);
153+
buf.writeInt32LE(+int32, 0);
154+
return buf.toString('hex');
155+
}
156+
157+
exports.int32LEToHex = int32LEToHex;
158+
131159
/**
132160
* A helper to calculate the byte size of a string (including null)
133161
*

0 commit comments

Comments
 (0)