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

fix(eval): improve security of safe-eval #233

Merged
merged 2 commits into from
Nov 17, 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
2 changes: 1 addition & 1 deletion badges/coverage-badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion badges/licenses-badge-dev.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion badges/tests-badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 13 additions & 22 deletions dist/index-browser-esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,7 @@ jsep.plugins.register(index, plugin);
jsep.addUnaryOp('typeof');
jsep.addLiteral('null', null);
jsep.addLiteral('undefined', undefined);
const BLOCKED_PROTO_PROPERTIES = new Set(['constructor', '__proto__', '__defineGetter__', '__defineSetter__']);
const SafeEval = {
/**
* @param {jsep.Expression} ast
Expand Down Expand Up @@ -1285,7 +1286,7 @@ const SafeEval = {
return SafeEval.evalAst(ast.alternate, subs);
},
evalIdentifier(ast, subs) {
if (ast.name in subs) {
if (Object.hasOwn(subs, ast.name)) {
return subs[ast.name];
}
throw ReferenceError(`${ast.name} is not defined`);
Expand All @@ -1294,23 +1295,17 @@ const SafeEval = {
return ast.value;
},
evalMemberExpression(ast, subs) {
if (ast.property.type === 'Identifier' && ast.property.name === 'constructor' || ast.object.type === 'Identifier' && ast.object.name === 'constructor') {
throw new Error("'constructor' property is disabled");
}
const prop = ast.computed ? SafeEval.evalAst(ast.property) // `object[property]`
: ast.property.name; // `object.property` property is Identifier
const obj = SafeEval.evalAst(ast.object, subs);
if (obj === undefined || obj === null) {
throw TypeError(`Cannot read properties of ${obj} (reading '${prop}')`);
}
if (!Object.hasOwn(obj, prop) && BLOCKED_PROTO_PROPERTIES.has(prop)) {
throw TypeError(`Cannot read properties of ${obj} (reading '${prop}')`);
}
const result = obj[prop];
if (typeof result === 'function') {
if (obj === Function && prop === 'bind') {
throw new Error('Function.prototype.bind is disabled');
}
if (obj === Function && (prop === 'call' || prop === 'apply')) {
throw new Error('Function.prototype.call and ' + 'Function.prototype.apply are disabled');
}
if (result === Function) {
return result; // Don't bind so can identify and throw later
}
return result.bind(obj); // arrow functions aren't affected by bind.
}
return result;
Expand All @@ -1332,19 +1327,16 @@ const SafeEval = {
evalCallExpression(ast, subs) {
const args = ast.arguments.map(arg => SafeEval.evalAst(arg, subs));
const func = SafeEval.evalAst(ast.callee, subs);
if (func === Function) {
throw new Error('Function constructor is disabled');
}
// if (func === Function) {
// throw new Error('Function constructor is disabled');
// }
return func(...args);
},
evalAssignmentExpression(ast, subs) {
if (ast.left.type !== 'Identifier') {
throw SyntaxError('Invalid left-hand side in assignment');
}
const id = ast.left.name;
if (id === '__proto__') {
throw new Error('Assignment to __proto__ is disabled');
}
const value = SafeEval.evalAst(ast.right, subs);
subs[id] = value;
return subs[id];
Expand All @@ -1369,9 +1361,8 @@ class SafeScript {
* @returns {EvaluatedResult} Result of evaluated code
*/
runInNewContext(context) {
const keyMap = {
...context
};
// `Object.create(null)` creates a prototypeless object
const keyMap = Object.assign(Object.create(null), context);
return SafeEval.evalAst(this.ast, keyMap);
}
}
Expand Down
2 changes: 1 addition & 1 deletion dist/index-browser-esm.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index-browser-esm.min.js.map

Large diffs are not rendered by default.

35 changes: 13 additions & 22 deletions dist/index-browser-umd.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,7 @@
jsep.addUnaryOp('typeof');
jsep.addLiteral('null', null);
jsep.addLiteral('undefined', undefined);
const BLOCKED_PROTO_PROPERTIES = new Set(['constructor', '__proto__', '__defineGetter__', '__defineSetter__']);
const SafeEval = {
/**
* @param {jsep.Expression} ast
Expand Down Expand Up @@ -1291,7 +1292,7 @@
return SafeEval.evalAst(ast.alternate, subs);
},
evalIdentifier(ast, subs) {
if (ast.name in subs) {
if (Object.hasOwn(subs, ast.name)) {
return subs[ast.name];
}
throw ReferenceError(`${ast.name} is not defined`);
Expand All @@ -1300,23 +1301,17 @@
return ast.value;
},
evalMemberExpression(ast, subs) {
if (ast.property.type === 'Identifier' && ast.property.name === 'constructor' || ast.object.type === 'Identifier' && ast.object.name === 'constructor') {
throw new Error("'constructor' property is disabled");
}
const prop = ast.computed ? SafeEval.evalAst(ast.property) // `object[property]`
: ast.property.name; // `object.property` property is Identifier
const obj = SafeEval.evalAst(ast.object, subs);
if (obj === undefined || obj === null) {
throw TypeError(`Cannot read properties of ${obj} (reading '${prop}')`);
}
if (!Object.hasOwn(obj, prop) && BLOCKED_PROTO_PROPERTIES.has(prop)) {
throw TypeError(`Cannot read properties of ${obj} (reading '${prop}')`);
}
const result = obj[prop];
if (typeof result === 'function') {
if (obj === Function && prop === 'bind') {
throw new Error('Function.prototype.bind is disabled');
}
if (obj === Function && (prop === 'call' || prop === 'apply')) {
throw new Error('Function.prototype.call and ' + 'Function.prototype.apply are disabled');
}
if (result === Function) {
return result; // Don't bind so can identify and throw later
}
return result.bind(obj); // arrow functions aren't affected by bind.
}
return result;
Expand All @@ -1338,19 +1333,16 @@
evalCallExpression(ast, subs) {
const args = ast.arguments.map(arg => SafeEval.evalAst(arg, subs));
const func = SafeEval.evalAst(ast.callee, subs);
if (func === Function) {
throw new Error('Function constructor is disabled');
}
// if (func === Function) {
// throw new Error('Function constructor is disabled');
// }
return func(...args);
},
evalAssignmentExpression(ast, subs) {
if (ast.left.type !== 'Identifier') {
throw SyntaxError('Invalid left-hand side in assignment');
}
const id = ast.left.name;
if (id === '__proto__') {
throw new Error('Assignment to __proto__ is disabled');
}
const value = SafeEval.evalAst(ast.right, subs);
subs[id] = value;
return subs[id];
Expand All @@ -1375,9 +1367,8 @@
* @returns {EvaluatedResult} Result of evaluated code
*/
runInNewContext(context) {
const keyMap = {
...context
};
// `Object.create(null)` creates a prototypeless object
const keyMap = Object.assign(Object.create(null), context);
return SafeEval.evalAst(this.ast, keyMap);
}
}
Expand Down
2 changes: 1 addition & 1 deletion dist/index-browser-umd.min.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index-browser-umd.min.cjs.map

Large diffs are not rendered by default.

35 changes: 13 additions & 22 deletions dist/index-node-cjs.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,7 @@ jsep.plugins.register(index, plugin);
jsep.addUnaryOp('typeof');
jsep.addLiteral('null', null);
jsep.addLiteral('undefined', undefined);
const BLOCKED_PROTO_PROPERTIES = new Set(['constructor', '__proto__', '__defineGetter__', '__defineSetter__']);
const SafeEval = {
/**
* @param {jsep.Expression} ast
Expand Down Expand Up @@ -1286,7 +1287,7 @@ const SafeEval = {
return SafeEval.evalAst(ast.alternate, subs);
},
evalIdentifier(ast, subs) {
if (ast.name in subs) {
if (Object.hasOwn(subs, ast.name)) {
return subs[ast.name];
}
throw ReferenceError(`${ast.name} is not defined`);
Expand All @@ -1295,23 +1296,17 @@ const SafeEval = {
return ast.value;
},
evalMemberExpression(ast, subs) {
if (ast.property.type === 'Identifier' && ast.property.name === 'constructor' || ast.object.type === 'Identifier' && ast.object.name === 'constructor') {
throw new Error("'constructor' property is disabled");
}
const prop = ast.computed ? SafeEval.evalAst(ast.property) // `object[property]`
: ast.property.name; // `object.property` property is Identifier
const obj = SafeEval.evalAst(ast.object, subs);
if (obj === undefined || obj === null) {
throw TypeError(`Cannot read properties of ${obj} (reading '${prop}')`);
}
if (!Object.hasOwn(obj, prop) && BLOCKED_PROTO_PROPERTIES.has(prop)) {
throw TypeError(`Cannot read properties of ${obj} (reading '${prop}')`);
}
const result = obj[prop];
if (typeof result === 'function') {
if (obj === Function && prop === 'bind') {
throw new Error('Function.prototype.bind is disabled');
}
if (obj === Function && (prop === 'call' || prop === 'apply')) {
throw new Error('Function.prototype.call and ' + 'Function.prototype.apply are disabled');
}
if (result === Function) {
return result; // Don't bind so can identify and throw later
}
return result.bind(obj); // arrow functions aren't affected by bind.
}
return result;
Expand All @@ -1333,19 +1328,16 @@ const SafeEval = {
evalCallExpression(ast, subs) {
const args = ast.arguments.map(arg => SafeEval.evalAst(arg, subs));
const func = SafeEval.evalAst(ast.callee, subs);
if (func === Function) {
throw new Error('Function constructor is disabled');
}
// if (func === Function) {
// throw new Error('Function constructor is disabled');
// }
return func(...args);
},
evalAssignmentExpression(ast, subs) {
if (ast.left.type !== 'Identifier') {
throw SyntaxError('Invalid left-hand side in assignment');
}
const id = ast.left.name;
if (id === '__proto__') {
throw new Error('Assignment to __proto__ is disabled');
}
const value = SafeEval.evalAst(ast.right, subs);
subs[id] = value;
return subs[id];
Expand All @@ -1370,9 +1362,8 @@ class SafeScript {
* @returns {EvaluatedResult} Result of evaluated code
*/
runInNewContext(context) {
const keyMap = {
...context
};
// `Object.create(null)` creates a prototypeless object
const keyMap = Object.assign(Object.create(null), context);
return SafeEval.evalAst(this.ast, keyMap);
}
}
Expand Down
Loading