diff --git a/package-lock.json b/package-lock.json index 44cf676..6533995 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ejson-shell-parser", - "version": "1.2.3", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ejson-shell-parser", - "version": "1.2.3", + "version": "2.0.0", "license": "MIT", "dependencies": { "acorn": "^8.1.0" @@ -30,8 +30,11 @@ "ts-jest": "^26.5.4", "typescript": "^4.3.5" }, + "engines": { + "node": ">=16" + }, "peerDependencies": { - "bson": "^4.6.3 || ^5.0.0" + "bson": "^4.6.3 || ^5 || ^6" } }, "node_modules/@ampproject/remapping": { diff --git a/src/check.ts b/src/check.ts index 5d21121..d1dc03b 100644 --- a/src/check.ts +++ b/src/check.ts @@ -56,7 +56,7 @@ class Checker { checkSafeExpression = (node: Node): boolean => { switch (node.type) { case 'Identifier': - return GLOBALS.hasOwnProperty(node.name); + return Object.prototype.hasOwnProperty.call(GLOBALS, node.name); case 'Literal': return true; case 'ArrayExpression': diff --git a/src/eval.ts b/src/eval.ts index f90f5ed..82004f8 100644 --- a/src/eval.ts +++ b/src/eval.ts @@ -116,7 +116,7 @@ const functionExpression = ( const walk = (node: Node): any => { switch (node.type) { case 'Identifier': - if (GLOBALS.hasOwnProperty(node.name)) { + if (Object.prototype.hasOwnProperty.call(GLOBALS, node.name)) { return GLOBALS[node.name]; } throw new Error(`${node.name} is not a valid Identifier`); @@ -133,7 +133,7 @@ const walk = (node: Node): any => { case 'NewExpression': return memberExpression(node, true); case 'ObjectExpression': - const obj: { [key: string]: any } = {}; + const obj: { [key: string]: any } = Object.create(null); node.properties.forEach(property => { const key = property.key.type === 'Identifier' @@ -141,7 +141,7 @@ const walk = (node: Node): any => { : walk(property.key); obj[key] = walk(property.value); }); - return obj; + return { ...obj }; case 'FunctionExpression': case 'ArrowFunctionExpression': return functionExpression(node); diff --git a/src/scope.ts b/src/scope.ts index b3eb413..c2d34b3 100644 --- a/src/scope.ts +++ b/src/scope.ts @@ -1,5 +1,12 @@ import * as bson from 'bson'; +// Returns the same object but frozen and with a null prototype. +function lookupMap(input: T): Readonly { + return Object.freeze( + Object.create(null, Object.getOwnPropertyDescriptors(input)) + ); +} + function NumberLong(v: any) { if (typeof v === 'string') { return bson.Long.fromString(v); @@ -8,25 +15,25 @@ function NumberLong(v: any) { } } -const SCOPE_CALL: { [x: string]: Function } = { +const SCOPE_CALL: { [x: string]: Function } = lookupMap({ Date: function(...args: any[]) { // casting our arguments as an empty array because we don't know // the length of our arguments, and should allow users to pass what // they want as date arguments return Date(...(args as [])); }, -}; +}); -const SCOPE_NEW: { [x: string]: Function } = { +const SCOPE_NEW: { [x: string]: Function } = lookupMap({ Date: function(...args: any[]) { // casting our arguments as an empty array because we don't know // the length of our arguments, and should allow users to pass what // they want as date arguments return new Date(...(args as [])); }, -}; +}); -const SCOPE_ANY: { [x: string]: Function } = { +const SCOPE_ANY: { [x: string]: Function } = lookupMap({ RegExp: RegExp, Binary: function(buffer: any, subType: any) { return new bson.Binary(buffer, subType); @@ -102,9 +109,9 @@ const SCOPE_ANY: { [x: string]: Function } = { // they want as date arguments return new Date(...(args as [])); }, -}; +}); -export const GLOBALS: { [x: string]: any } = Object.freeze({ +export const GLOBALS: { [x: string]: any } = lookupMap({ Infinity: Infinity, NaN: NaN, undefined: undefined, @@ -125,10 +132,10 @@ type ClassExpressions = { }; }; -const ALLOWED_CLASS_EXPRESSIONS: ClassExpressions = { - Math: { +const ALLOWED_CLASS_EXPRESSIONS: ClassExpressions = lookupMap({ + Math: lookupMap({ class: Math, - allowedMethods: { + allowedMethods: lookupMap({ abs: true, acos: true, acosh: true, @@ -163,11 +170,11 @@ const ALLOWED_CLASS_EXPRESSIONS: ClassExpressions = { tan: true, tanh: true, trunc: true, - }, - }, - Date: { + }), + }), + Date: lookupMap({ class: Date, - allowedMethods: { + allowedMethods: lookupMap({ getDate: true, getDay: true, getFullYear: true, @@ -205,13 +212,13 @@ const ALLOWED_CLASS_EXPRESSIONS: ClassExpressions = { setUTCSeconds: true, setYear: true, toISOString: true, - }, - }, - ISODate: { + }), + }), + ISODate: lookupMap({ class: Date, allowedMethods: 'Date', - }, -}; + }), +}); export const GLOBAL_FUNCTIONS = Object.freeze([ ...Object.keys(SCOPE_ANY), diff --git a/test/index.spec.ts b/test/index.spec.ts index ecba92c..96d104d 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -156,6 +156,19 @@ it('should not allow calling functions that do not exist', function() { expect(parse('{ date: require("") }')).toEqual(''); }); +for (const mode of [ParseMode.Extended, ParseMode.Strict, ParseMode.Loose]) { + it('should not allow calling functions that only exist as Object.prototype properties', function() { + expect(parse('{ date: Date.constructor("") }', { mode })).toEqual(''); + expect(parse('{ date: Date.hasOwnProperty("") }', { mode })).toEqual(''); + expect(parse('{ date: Date.__proto__("") }', { mode })).toEqual(''); + expect( + parse('{ date: Code({ toString: Date.constructor("throw null;") }) }', { + mode, + }) + ).toEqual(''); + }); +} + describe('Function calls', function() { const options: Partial = { mode: ParseMode.Strict,