Skip to content

Commit

Permalink
fix(NODE-4960): UUID validation too strict (#572)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbbeeken authored Apr 24, 2023
1 parent 91b84ed commit d239cd1
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 131 deletions.
87 changes: 47 additions & 40 deletions src/binary.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { bufferToUuidHexString, uuidHexStringToBuffer, uuidValidateString } from './uuid_utils';
import { isUint8Array } from './parser/utils';
import type { EJSONOptions } from './extended_json';
import { BSONError } from './error';
Expand Down Expand Up @@ -288,7 +287,7 @@ export class Binary extends BSONValue {
}
} else if ('$uuid' in doc) {
type = 4;
data = uuidHexStringToBuffer(doc.$uuid);
data = UUID.bytesFromString(doc.$uuid);
}
if (!data) {
throw new BSONError(`Unexpected Binary Extended JSON format ${JSON.stringify(doc)}`);
Expand All @@ -311,42 +310,40 @@ export class Binary extends BSONValue {
export type UUIDExtended = {
$uuid: string;
};

const UUID_BYTE_LENGTH = 16;
const UUID_WITHOUT_DASHES = /^[0-9A-F]{32}$/i;
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;

/**
* A class representation of the BSON UUID type.
* @public
*/
export class UUID extends Binary {
static cacheHexString: boolean;

/** UUID hexString cache @internal */
private __id?: string;

static cacheHexString = false;
/**
* Create an UUID type
* Create a UUID type
*
* When the argument to the constructor is omitted a random v4 UUID will be generated.
*
* @param input - Can be a 32 or 36 character hex string (dashes excluded/included) or a 16 byte binary Buffer.
*/
constructor(input?: string | Uint8Array | UUID) {
let bytes: Uint8Array;
let hexStr;
if (input == null) {
bytes = UUID.generate();
} else if (input instanceof UUID) {
bytes = ByteUtils.toLocalBufferType(new Uint8Array(input.buffer));
hexStr = input.__id;
} else if (ArrayBuffer.isView(input) && input.byteLength === UUID_BYTE_LENGTH) {
bytes = ByteUtils.toLocalBufferType(input);
} else if (typeof input === 'string') {
bytes = uuidHexStringToBuffer(input);
bytes = UUID.bytesFromString(input);
} else {
throw new BSONError(
'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).'
);
}
super(bytes, BSON_BINARY_SUBTYPE_UUID_NEW);
this.__id = hexStr;
}

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

set id(value: Uint8Array) {
this.buffer = value;

if (UUID.cacheHexString) {
this.__id = bufferToUuidHexString(value);
}
}

/**
* Returns the UUID id as a 32 or 36 character hex string representation, excluding/including dashes (defaults to 36 character dash separated)
* @param includeDashes - should the string exclude dash-separators.
* */
toHexString(includeDashes = true): string {
if (UUID.cacheHexString && this.__id) {
return this.__id;
}

const uuidHexString = bufferToUuidHexString(this.id, includeDashes);

if (UUID.cacheHexString) {
this.__id = uuidHexString;
if (includeDashes) {
return [
ByteUtils.toHex(this.buffer.subarray(0, 4)),
ByteUtils.toHex(this.buffer.subarray(4, 6)),
ByteUtils.toHex(this.buffer.subarray(6, 8)),
ByteUtils.toHex(this.buffer.subarray(8, 10)),
ByteUtils.toHex(this.buffer.subarray(10, 16))
].join('-');
}

return uuidHexString;
return ByteUtils.toHex(this.buffer);
}

/**
Expand Down Expand Up @@ -446,37 +438,32 @@ export class UUID extends Binary {
* Checks if a value is a valid bson UUID
* @param input - UUID, string or Buffer to validate.
*/
static isValid(input: string | Uint8Array | UUID): boolean {
static isValid(input: string | Uint8Array | UUID | Binary): boolean {
if (!input) {
return false;
}

if (input instanceof UUID) {
return true;
}

if (typeof input === 'string') {
return uuidValidateString(input);
return UUID.isValidUUIDString(input);
}

if (isUint8Array(input)) {
// check for length & uuid version (https://tools.ietf.org/html/rfc4122#section-4.1.3)
if (input.byteLength !== UUID_BYTE_LENGTH) {
return false;
}

return (input[6] & 0xf0) === 0x40 && (input[8] & 0x80) === 0x80;
return input.byteLength === UUID_BYTE_LENGTH;
}

return false;
return (
input._bsontype === 'Binary' &&
input.sub_type === this.SUBTYPE_UUID &&
input.buffer.byteLength === 16
);
}

/**
* Creates an UUID from a hex string representation of an UUID.
* @param hexString - 32 or 36 character hex string (dashes excluded/included).
*/
static override createFromHexString(hexString: string): UUID {
const buffer = uuidHexStringToBuffer(hexString);
const buffer = UUID.bytesFromString(hexString);
return new UUID(buffer);
}

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

/** @internal */
static bytesFromString(representation: string) {
if (!UUID.isValidUUIDString(representation)) {
throw new BSONError(
'UUID string representation must be 32 hex digits or canonical hyphenated representation'
);
}
return ByteUtils.fromHex(representation.replace(/-/g, ''));
}

/**
* @internal
*
* Validates a string to be a hex digit sequence with or without dashes.
* The canonical hyphenated representation of a uuid is hex in 8-4-4-4-12 groups.
*/
static isValidUUIDString(representation: string) {
return UUID_WITHOUT_DASHES.test(representation) || UUID_WITH_DASHES.test(representation);
}

/**
* Converts to a string representation of this Id.
*
Expand Down
9 changes: 5 additions & 4 deletions src/parser/deserializer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Binary } from '../binary';
import type { Document } from '../bson';
import { Document, UUID } from '../bson';
import { Code } from '../code';
import * as constants from '../constants';
import { DBRef, DBRefLike, isDBRefLike } from '../db_ref';
Expand Down Expand Up @@ -404,7 +404,7 @@ function deserializeObject(
value = ByteUtils.toLocalBufferType(buffer.slice(index, index + binarySize));
} else {
value = new Binary(buffer.slice(index, index + binarySize), subType);
if (subType === constants.BSON_BINARY_SUBTYPE_UUID_NEW) {
if (subType === constants.BSON_BINARY_SUBTYPE_UUID_NEW && UUID.isValid(value)) {
value = value.toUUID();
}
}
Expand Down Expand Up @@ -432,10 +432,11 @@ function deserializeObject(

if (promoteBuffers && promoteValues) {
value = _buffer;
} else if (subType === constants.BSON_BINARY_SUBTYPE_UUID_NEW) {
value = new Binary(buffer.slice(index, index + binarySize), subType).toUUID();
} else {
value = new Binary(buffer.slice(index, index + binarySize), subType);
if (subType === constants.BSON_BINARY_SUBTYPE_UUID_NEW && UUID.isValid(value)) {
value = value.toUUID();
}
}
}

Expand Down
33 changes: 0 additions & 33 deletions src/uuid_utils.ts

This file was deleted.

28 changes: 28 additions & 0 deletions test/node/tools/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,34 @@ const bufferFromHexArray = array => {

exports.bufferFromHexArray = bufferFromHexArray;

/**
* A companion helper to bufferFromHexArray to help with constructing bson bytes manually.
* When creating a BSON Binary you need a leading little endian int32 followed by a sequence of bytes
* of that length.
*
* @example
* ```js
* const binAsHex = '000000';
* const serializedUUID = bufferFromHexArray([
* '05', // binData type
* '6100', // 'a' & null
* int32ToHex(binAsHex.length / 2), // binary starts with int32 length
* '7F', // user subtype
* binAsHex // uuid bytes
* ]);
* ```
*
* @param {number | Int32} int32 -
* @returns
*/
function int32LEToHex(int32) {
const buf = Buffer.alloc(4);
buf.writeInt32LE(+int32, 0);
return buf.toString('hex');
}

exports.int32LEToHex = int32LEToHex;

/**
* A helper to calculate the byte size of a string (including null)
*
Expand Down
Loading

0 comments on commit d239cd1

Please sign in to comment.