From 949131693f19ec58f738b626a79d074f675219e6 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Fri, 24 May 2024 14:04:49 -0300 Subject: [PATCH 1/2] fix(security): sanitize fields and tables when using nestTables --- lib/helpers.js | 12 ++++++ lib/parsers/binary_parser.js | 21 +++------- lib/parsers/text_parser.js | 24 +++++------- .../ensure-safe-binary-fields.test.mjs | 39 +++++++++++++++++-- .../parsers/ensure-safe-text-fields.test.mjs | 39 +++++++++++++++++-- 5 files changed, 100 insertions(+), 35 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index fdba2ddb76..55a52bb461 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -73,3 +73,15 @@ const privateObjectProps = new Set([ ]); exports.privateObjectProps = privateObjectProps; + +const fieldEscape = (field) => { + if (privateObjectProps.has(field)) { + throw new Error( + `The field name (${field}) can't be the same as an object's private property.`, + ); + } + + return srcEscape(field); +}; + +exports.fieldEscape = fieldEscape; diff --git a/lib/parsers/binary_parser.js b/lib/parsers/binary_parser.js index 9d53d8c188..a3b9e7ebe8 100644 --- a/lib/parsers/binary_parser.js +++ b/lib/parsers/binary_parser.js @@ -145,26 +145,18 @@ function compile(fields, options, config) { let tableName = ''; for (let i = 0; i < fields.length; i++) { - fieldName = helpers.srcEscape(fields[i].name); - - if (helpers.privateObjectProps.has(fields[i].name)) { - throw new Error( - `The field name (${fieldName}) can't be the same as an object's private property.`, - ); - } - - parserFn(`// ${fieldName}: ${typeNames[fields[i].columnType]}`); + fieldName = helpers.fieldEscape(fields[i].name); + // parserFn(`// ${fieldName}: ${typeNames[fields[i].columnType]}`); if (typeof options.nestTables === 'string') { - lvalue = `result[${helpers.srcEscape( - fields[i].table + options.nestTables + fields[i].name, - )}]`; + lvalue = `result[${helpers.fieldEscape(fields[i].table + options.nestTables + fields[i].name)}]`; } else if (options.nestTables === true) { - tableName = helpers.srcEscape(fields[i].table); + tableName = helpers.fieldEscape(fields[i].table); + parserFn(`if (!result[${tableName}]) result[${tableName}] = {};`); lvalue = `result[${tableName}][${fieldName}]`; } else if (options.rowsAsArray) { - lvalue = `result[${i.toString(10)}]`; + lvalue = `result[${helpers.fieldEscape(i.toString(10))}]`; } else { lvalue = `result[${fieldName}]`; } @@ -190,7 +182,6 @@ function compile(fields, options, config) { } parserFn('}'); - currentFieldNullBit *= 2; if (currentFieldNullBit === 0x100) { currentFieldNullBit = 1; diff --git a/lib/parsers/text_parser.js b/lib/parsers/text_parser.js index be89e77123..9041db2cac 100644 --- a/lib/parsers/text_parser.js +++ b/lib/parsers/text_parser.js @@ -143,30 +143,26 @@ function compile(fields, options, config) { } resultTablesArray = Object.keys(resultTables); for (let i = 0; i < resultTablesArray.length; i++) { - parserFn(`result[${helpers.srcEscape(resultTablesArray[i])}] = {};`); + parserFn(`result[${helpers.fieldEscape(resultTablesArray[i])}] = {};`); } } let lvalue = ''; let fieldName = ''; + let tableName = ''; for (let i = 0; i < fields.length; i++) { - fieldName = helpers.srcEscape(fields[i].name); + fieldName = helpers.fieldEscape(fields[i].name); + // parserFn(`// ${fieldName}: ${typeNames[fields[i].columnType]}`); - if (helpers.privateObjectProps.has(fields[i].name)) { - throw new Error( - `The field name (${fieldName}) can't be the same as an object's private property.`, - ); - } - - parserFn(`// ${fieldName}: ${typeNames[fields[i].columnType]}`); if (typeof options.nestTables === 'string') { - lvalue = `result[${helpers.srcEscape( - fields[i].table + options.nestTables + fields[i].name, - )}]`; + lvalue = `result[${helpers.fieldEscape(fields[i].table + options.nestTables + fields[i].name)}]`; } else if (options.nestTables === true) { - lvalue = `result[${helpers.srcEscape(fields[i].table)}][${fieldName}]`; + tableName = helpers.fieldEscape(fields[i].table); + + parserFn(`if (!result[${tableName}]) result[${tableName}] = {};`); + lvalue = `result[${tableName}][${fieldName}]`; } else if (options.rowsAsArray) { - lvalue = `result[${i.toString(10)}]`; + lvalue = `result[${helpers.fieldEscape(i.toString(10))}]`; } else { lvalue = `result[${fieldName}]`; } diff --git a/test/esm/unit/parsers/ensure-safe-binary-fields.test.mjs b/test/esm/unit/parsers/ensure-safe-binary-fields.test.mjs index a16cc43014..3cce43c1ec 100644 --- a/test/esm/unit/parsers/ensure-safe-binary-fields.test.mjs +++ b/test/esm/unit/parsers/ensure-safe-binary-fields.test.mjs @@ -1,13 +1,12 @@ import { describe, assert } from 'poku'; import { describeOptions } from '../../../common.test.cjs'; import getBinaryParser from '../../../../lib/parsers/binary_parser.js'; -import { srcEscape } from '../../../../lib/helpers.js'; import { privateObjectProps } from '../../../../lib/helpers.js'; describe('Binary Parser: Block Native Object Props', describeOptions); const blockedFields = Array.from(privateObjectProps).map((prop) => [ - { name: prop }, + { name: prop, table: '' }, ]); blockedFields.forEach((fields) => { @@ -17,8 +16,42 @@ blockedFields.forEach((fields) => { } catch (error) { assert.strictEqual( error.message, - `The field name (${srcEscape(fields[0].name)}) can't be the same as an object's private property.`, + `The field name (${fields[0].name}) can't be the same as an object's private property.`, `Ensure safe ${fields[0].name}`, ); } }); + +blockedFields + .map((fields) => + fields.map((field) => ({ ...field, name: field.name.slice(1) })), + ) + .forEach((fields) => { + try { + getBinaryParser(fields, { nestTables: '_' }, {}); + assert.fail('An error was expected'); + } catch (error) { + assert.strictEqual( + error.message, + `The field name (_${fields[0].name}) can't be the same as an object's private property.`, + `Ensure safe _${fields[0].name} for nestTables as string`, + ); + } + }); + +blockedFields + .map((fields) => + fields.map((field) => ({ ...field, name: '', table: field.name })), + ) + .forEach((fields) => { + try { + getBinaryParser(fields, { nestTables: true }, {}); + assert.fail('An error was expected'); + } catch (error) { + assert.strictEqual( + error.message, + `The field name (${fields[0].table}) can't be the same as an object's private property.`, + `Ensure safe ${fields[0].table} for nestTables as true`, + ); + } + }); diff --git a/test/esm/unit/parsers/ensure-safe-text-fields.test.mjs b/test/esm/unit/parsers/ensure-safe-text-fields.test.mjs index 36ef7f4516..2ea4b04b77 100644 --- a/test/esm/unit/parsers/ensure-safe-text-fields.test.mjs +++ b/test/esm/unit/parsers/ensure-safe-text-fields.test.mjs @@ -1,13 +1,12 @@ import { describe, assert } from 'poku'; import { describeOptions } from '../../../common.test.cjs'; import TextRowParser from '../../../../lib/parsers/text_parser.js'; -import { srcEscape } from '../../../../lib/helpers.js'; import { privateObjectProps } from '../../../../lib/helpers.js'; describe('Text Parser: Block Native Object Props', describeOptions); const blockedFields = Array.from(privateObjectProps).map((prop) => [ - { name: prop }, + { name: prop, table: '' }, ]); blockedFields.forEach((fields) => { @@ -17,8 +16,42 @@ blockedFields.forEach((fields) => { } catch (error) { assert.strictEqual( error.message, - `The field name (${srcEscape(fields[0].name)}) can't be the same as an object's private property.`, + `The field name (${fields[0].name}) can't be the same as an object's private property.`, `Ensure safe ${fields[0].name}`, ); } }); + +blockedFields + .map((fields) => + fields.map((field) => ({ ...field, name: field.name.slice(1) })), + ) + .forEach((fields) => { + try { + TextRowParser(fields, { nestTables: '_' }, {}); + assert.fail('An error was expected'); + } catch (error) { + assert.strictEqual( + error.message, + `The field name (_${fields[0].name}) can't be the same as an object's private property.`, + `Ensure safe _${fields[0].name} for nestTables as string`, + ); + } + }); + +blockedFields + .map((fields) => + fields.map((field) => ({ ...field, name: '', table: field.name })), + ) + .forEach((fields) => { + try { + TextRowParser(fields, { nestTables: true }, {}); + assert.fail('An error was expected'); + } catch (error) { + assert.strictEqual( + error.message, + `The field name (${fields[0].table}) can't be the same as an object's private property.`, + `Ensure safe ${fields[0].table} for nestTables as true`, + ); + } + }); From 1bde405eaa8f34690b26bc96f44309de4da2bcf6 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Sat, 25 May 2024 21:42:38 -0300 Subject: [PATCH 2/2] chore: undone `nestTables` validation for `rowsAsArray` --- lib/parsers/binary_parser.js | 2 +- lib/parsers/text_parser.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/parsers/binary_parser.js b/lib/parsers/binary_parser.js index a3b9e7ebe8..59d87bca32 100644 --- a/lib/parsers/binary_parser.js +++ b/lib/parsers/binary_parser.js @@ -156,7 +156,7 @@ function compile(fields, options, config) { parserFn(`if (!result[${tableName}]) result[${tableName}] = {};`); lvalue = `result[${tableName}][${fieldName}]`; } else if (options.rowsAsArray) { - lvalue = `result[${helpers.fieldEscape(i.toString(10))}]`; + lvalue = `result[${i.toString(10)}]`; } else { lvalue = `result[${fieldName}]`; } diff --git a/lib/parsers/text_parser.js b/lib/parsers/text_parser.js index 9041db2cac..acd75caca7 100644 --- a/lib/parsers/text_parser.js +++ b/lib/parsers/text_parser.js @@ -162,7 +162,7 @@ function compile(fields, options, config) { parserFn(`if (!result[${tableName}]) result[${tableName}] = {};`); lvalue = `result[${tableName}][${fieldName}]`; } else if (options.rowsAsArray) { - lvalue = `result[${helpers.fieldEscape(i.toString(10))}]`; + lvalue = `result[${i.toString(10)}]`; } else { lvalue = `result[${fieldName}]`; }