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

chore: prefer frozen objects with null prototype as dictionary objects #198

Merged
merged 2 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
45 changes: 26 additions & 19 deletions src/scope.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
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 +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);
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
13 changes: 13 additions & 0 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Options> = {
mode: ParseMode.Strict,
Expand Down
Loading