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

feat(NODE-3740): Implement root and top level key utf-8 validation settings for BSON #472

Merged
merged 13 commits into from
Nov 19, 2021
Merged
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

139 changes: 122 additions & 17 deletions src/parser/deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ export interface DeserializeOptions {
index?: number;

raw?: boolean;
/** allows for opt-out utf-8 validation for all keys or
* specified keys. Must be all true or all false.
dariakp marked this conversation as resolved.
Show resolved Hide resolved
*
* @example
* ```js
* // disables validation on all keys
* validation: { utf8: false }
*
* // enables validation on specified keys a, b, and c
dariakp marked this conversation as resolved.
Show resolved Hide resolved
* validation: { utf8: { a: true, b: true, c: true } }
* ```
*/
validation?: { utf8: boolean | Record<string, boolean> };
dariakp marked this conversation as resolved.
Show resolved Hide resolved
}

// Internal long versions
Expand Down Expand Up @@ -102,7 +115,8 @@ function deserializeObject(
buffer: Buffer,
index: number,
options: DeserializeOptions,
isArray = false
isArray = false,
nestedKey?: boolean
dariakp marked this conversation as resolved.
Show resolved Hide resolved
) {
const evalFunctions = options['evalFunctions'] == null ? false : options['evalFunctions'];
const cacheFunctions = options['cacheFunctions'] == null ? false : options['cacheFunctions'];
Expand All @@ -120,6 +134,49 @@ function deserializeObject(
const promoteLongs = options['promoteLongs'] == null ? true : options['promoteLongs'];
const promoteValues = options['promoteValues'] == null ? true : options['promoteValues'];

// Ensures default validation option if none given
const validation = options.validation == null ? { utf8: true } : options.validation;

// Shows if global utf-8 validation is enabled or disabled
let globalUTFValidation = true;
// Reflects utf-8 validation setting regardless of global or specific key validation
let validationSetting: boolean;
// Set of keys either to enable or disable validation on
const utf8KeysSet = new Set();

// Check for boolean uniformity and empty validation option
const utf8ValidatedKeys = validation.utf8;
if (typeof utf8ValidatedKeys !== 'boolean') {
globalUTFValidation = false;
const utf8ValidationValues = Object.keys(utf8ValidatedKeys).map(function (key) {
return utf8ValidatedKeys[key];
});
if (utf8ValidationValues.length !== 0) {
dariakp marked this conversation as resolved.
Show resolved Hide resolved
// Ensures boolean uniformity in utf-8 validation (all true or all false)
if (typeof utf8ValidationValues[0] === 'boolean') {
validationSetting = utf8ValidationValues[0];
if (!utf8ValidationValues.every(item => item === validationSetting)) {
throw new BSONError(
'Invalid UTF-8 validation option - keys must be all true or all false'
);
}
} else {
throw new BSONError('Invalid UTF-8 validation option, must specify boolean values');
}
} else {
throw new BSONError('validation option is empty');
dariakp marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
validationSetting = utf8ValidatedKeys;
}

// Add keys to set that will either be validated or not based on validationSetting
if ((!validationSetting && !globalUTFValidation) || (validationSetting && !globalUTFValidation)) {
dariakp marked this conversation as resolved.
Show resolved Hide resolved
for (const key of Object.keys(utf8ValidatedKeys)) {
utf8KeysSet.add(key);
}
}

// Set the start index
const startIndex = index;

Expand Down Expand Up @@ -158,7 +215,26 @@ function deserializeObject(

// If are at the end of the buffer there is a problem with the document
if (i >= buffer.byteLength) throw new BSONError('Bad BSON Document: illegal CString');

// Represents the key
const name = isArray ? arrayIndex++ : buffer.toString('utf8', index, i);

// keyValidate is true if the key should be validated, false otherwise
let keyValidate = true;
dariakp marked this conversation as resolved.
Show resolved Hide resolved
if (globalUTFValidation) {
dariakp marked this conversation as resolved.
Show resolved Hide resolved
keyValidate = validationSetting;
} else {
if (utf8KeysSet.has(name)) {
keyValidate = validationSetting;
} else {
keyValidate = !validationSetting;
}
}
// if nested key, validate based on top level key
if (nestedKey != null) {
keyValidate = nestedKey;
}

if (isPossibleDBRef !== false && (name as string)[0] === '$') {
isPossibleDBRef = allowedDBRefKeys.test(name as string);
}
Expand All @@ -179,9 +255,7 @@ function deserializeObject(
) {
throw new BSONError('bad string length in bson');
}

value = getValidatedString(buffer, index, index + stringSize - 1);

value = getValidatedString(buffer, index, index + stringSize - 1, validation, keyValidate);
index = index + stringSize;
} else if (elementType === constants.BSON_DATA_OID) {
const oid = Buffer.alloc(12);
Expand Down Expand Up @@ -234,7 +308,7 @@ function deserializeObject(
if (raw) {
value = buffer.slice(index, index + objectSize);
} else {
value = deserializeObject(buffer, _index, options, false);
value = deserializeObject(buffer, _index, options, false, keyValidate);
}

index = index + objectSize;
Expand Down Expand Up @@ -263,7 +337,7 @@ function deserializeObject(
arrayOptions['raw'] = true;
}

value = deserializeObject(buffer, _index, arrayOptions, true);
value = deserializeObject(buffer, _index, arrayOptions, true, keyValidate);
index = index + objectSize;

if (buffer[index - 1] !== 0) throw new BSONError('invalid array terminator byte');
Expand Down Expand Up @@ -463,7 +537,13 @@ function deserializeObject(
) {
throw new BSONError('bad string length in bson');
}
const symbol = getValidatedString(buffer, index, index + stringSize - 1);
const symbol = getValidatedString(
buffer,
index,
index + stringSize - 1,
validation,
keyValidate
);
value = promoteValues ? symbol : new BSONSymbol(symbol);
index = index + stringSize;
} else if (elementType === constants.BSON_DATA_TIMESTAMP) {
Expand Down Expand Up @@ -496,7 +576,13 @@ function deserializeObject(
) {
throw new BSONError('bad string length in bson');
}
const functionString = getValidatedString(buffer, index, index + stringSize - 1);
const functionString = getValidatedString(
buffer,
index,
index + stringSize - 1,
validation,
keyValidate
);

// If we are evaluating the functions
if (evalFunctions) {
Expand Down Expand Up @@ -541,7 +627,13 @@ function deserializeObject(
}

// Javascript function
const functionString = getValidatedString(buffer, index, index + stringSize - 1);
const functionString = getValidatedString(
buffer,
index,
index + stringSize - 1,
validation,
keyValidate
);
// Update parse index position
index = index + stringSize;
// Parse the element
Expand Down Expand Up @@ -596,8 +688,10 @@ function deserializeObject(
)
throw new BSONError('bad string length in bson');
// Namespace
if (!validateUtf8(buffer, index, index + stringSize - 1)) {
throw new BSONError('Invalid UTF-8 string in BSON document');
if (validation != null && validation.utf8) {
if (!validateUtf8(buffer, index, index + stringSize - 1)) {
throw new BSONError('Invalid UTF-8 string in BSON document');
}
}
const namespace = buffer.toString('utf8', index, index + stringSize - 1);
// Update parse index position
Expand Down Expand Up @@ -670,14 +764,25 @@ function isolateEval(
return functionCache[functionString].bind(object);
}

function getValidatedString(buffer: Buffer, start: number, end: number) {
function getValidatedString(
buffer: Buffer,
start: number,
end: number,
validation: Document,
dariakp marked this conversation as resolved.
Show resolved Hide resolved
check: boolean
) {
const value = buffer.toString('utf8', start, end);
for (let i = 0; i < value.length; i++) {
if (value.charCodeAt(i) === 0xfffd) {
if (!validateUtf8(buffer, start, end)) {
throw new BSONError('Invalid UTF-8 string in BSON document');
// if utf8 validation is on, do the check
if (check) {
if (validation.utf8 != null && validation.utf8) {
dariakp marked this conversation as resolved.
Show resolved Hide resolved
for (let i = 0; i < value.length; i++) {
if (value.charCodeAt(i) === 0xfffd) {
if (!validateUtf8(buffer, start, end)) {
throw new BSONError('Invalid UTF-8 string in BSON document');
}
break;
}
}
break;
}
}
return value;
Expand Down
31 changes: 31 additions & 0 deletions test/node/tools/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,34 @@ const bufferFromHexArray = array => {
};

exports.bufferFromHexArray = bufferFromHexArray;

/**
* A helper to calculate the byte size of a string (including null)
*
* ```js
* const x = stringToUTF8HexBytes('ab') // { x: '03000000616200' }
*
* @param string - representing what you want to encode into BSON
* @returns BSON string with byte size encoded
*/
const stringToUTF8HexBytes = str => {
var b = Buffer.from(str, 'utf8');
var len = b.byteLength;
var out = Buffer.alloc(len + 4 + 1);
out.writeInt32LE(len + 1, 0);
out.set(b, 4);
out[len + 1] = 0x00;
return out.toString('hex');
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
};

exports.stringToUTF8HexBytes = stringToUTF8HexBytes;

exports.isBrowser = function () {
// eslint-disable-next-line no-undef
return typeof window === 'object' && typeof window['navigator'] === 'object';
};

exports.isNode6 = function () {
// eslint-disable-next-line no-undef
return process.version.split('.')[0] === 'v6';
};
Loading