Skip to content
This repository has been archived by the owner on Jul 9, 2024. It is now read-only.

Commit

Permalink
chore: prefer frozen objects with null prototype as dictionary objects
Browse files Browse the repository at this point in the history
  • Loading branch information
addaleax committed Feb 20, 2024
1 parent f205084 commit 927d137
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 26 deletions.
9 changes: 6 additions & 3 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion src/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
6 changes: 3 additions & 3 deletions src/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand All @@ -133,15 +133,15 @@ 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'
? property.key.name
: walk(property.key);
obj[key] = walk(property.value);
});
return obj;
return { ...obj };
case 'FunctionExpression':
case 'ArrowFunctionExpression':
return functionExpression(node);
Expand Down
43 changes: 24 additions & 19 deletions src/scope.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as bson from 'bson';

// Returns the same object but frozen and with a null prototype.
function lookupMap<T extends {}>(input: T): Readonly<T> {
return Object.freeze(Object.create(null, Object.getOwnPropertyDescriptors(input)));
}

function NumberLong(v: any) {
if (typeof v === 'string') {
return bson.Long.fromString(v);
Expand All @@ -8,25 +13,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);
Expand Down Expand Up @@ -102,9 +107,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,
Expand All @@ -125,10 +130,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,
Expand Down Expand Up @@ -163,11 +168,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,
Expand Down Expand Up @@ -205,13 +210,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),
Expand Down
9 changes: 9 additions & 0 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ 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<Options> = {
mode: ParseMode.Strict,
Expand Down

0 comments on commit 927d137

Please sign in to comment.