Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(NODE-4960): UUID validation too strict #572

Merged
merged 11 commits into from
Apr 24, 2023
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this safely be removed? It's a public property

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, added it back and filed follow ups, we can deprecate it in the subtask I made and remove in v6


/** 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) {
addaleax marked this conversation as resolved.
Show resolved Hide resolved
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