Skip to content

Commit fce6a82

Browse files
committed
fix(safe-eval): harden operator lookup against prototype inheritance
Replace plain operator tables with null-prototype objects and explicit own-property checks. This prevents prototype-chain access from bypassing the evaluator’s operator whitelist and keeps the sandbox behavior deterministic and safer.
1 parent 71597f7 commit fce6a82

1 file changed

Lines changed: 42 additions & 52 deletions

File tree

src/Safe-Script.js

Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,39 @@ const BLOCKED_PROTO_PROPERTIES = new Set([
4040
'__lookupSetter__'
4141
]);
4242

43+
const BINOPS = Object.assign(Object.create(null), {
44+
'||': (a, b) => a || b(),
45+
'&&': (a, b) => a && b(),
46+
'|': (a, b) => a | b(),
47+
'^': (a, b) => a ^ b(),
48+
'&': (a, b) => a & b(),
49+
'==': (a, b) => a == b(),
50+
'!=': (a, b) => a != b(),
51+
'===': (a, b) => a === b(),
52+
'!==': (a, b) => a !== b(),
53+
'<': (a, b) => a < b(),
54+
'>': (a, b) => a > b(),
55+
'<=': (a, b) => a <= b(),
56+
'>=': (a, b) => a >= b(),
57+
'<<': (a, b) => a << b(),
58+
'>>': (a, b) => a >> b(),
59+
'>>>': (a, b) => a >>> b(),
60+
'+': (a, b) => a + b(),
61+
'-': (a, b) => a - b(),
62+
'*': (a, b) => a * b(),
63+
'/': (a, b) => a / b(),
64+
'%': (a, b) => a % b()
65+
});
66+
67+
const UNOPS = Object.assign(Object.create(null), {
68+
'-': (a) => -(/** @type {EvaluatedResult} */ (a)),
69+
'!': (a) => !a,
70+
'~': (a) => ~(/** @type {EvaluatedResult} */ (a)),
71+
'+': (a) => +(/** @type {EvaluatedResult} */ (a)),
72+
typeof: (a) => typeof a,
73+
void: (a) => void a
74+
});
75+
4376
const SafeEval = {
4477
/**
4578
* @param {jsep.Expression} ast
@@ -109,36 +142,10 @@ const SafeEval = {
109142
* @returns {UnknownResult}
110143
*/
111144
evalBinaryExpression (ast, subs) {
112-
/**
113-
* @typedef {{
114-
* [key: string]: (a: AnyParameter, b: AnyParameter) => UnknownResult
115-
* }} OperatorTable
116-
*/
117-
const result = /** @type {OperatorTable} */ ({
118-
'||': (a, b) => a || b(),
119-
'&&': (a, b) => a && b(),
120-
'|': (a, b) => a | b(),
121-
'^': (a, b) => a ^ b(),
122-
'&': (a, b) => a & b(),
123-
// eslint-disable-next-line eqeqeq -- API
124-
'==': (a, b) => a == b(),
125-
// eslint-disable-next-line eqeqeq -- API
126-
'!=': (a, b) => a != b(),
127-
'===': (a, b) => a === b(),
128-
'!==': (a, b) => a !== b(),
129-
'<': (a, b) => a < b(),
130-
'>': (a, b) => a > b(),
131-
'<=': (a, b) => a <= b(),
132-
'>=': (a, b) => a >= b(),
133-
'<<': (a, b) => a << b(),
134-
'>>': (a, b) => a >> b(),
135-
'>>>': (a, b) => a >>> b(),
136-
'+': (a, b) => a + b(),
137-
'-': (a, b) => a - b(),
138-
'*': (a, b) => a * b(),
139-
'/': (a, b) => a / b(),
140-
'%': (a, b) => a % b()
141-
})[ast.operator](
145+
if (!Object.hasOwn(BINOPS, ast.operator)) {
146+
throw new SyntaxError(`Unknown binary operator: ${ast.operator}`);
147+
}
148+
const result = BINOPS[ast.operator](
142149
SafeEval.evalAst(ast.left, subs),
143150
() => SafeEval.evalAst(ast.right, subs)
144151
);
@@ -242,28 +249,11 @@ const SafeEval = {
242249
* @returns {UnknownResult}
243250
*/
244251
evalUnaryExpression (ast, subs) {
245-
/**
246-
* @typedef {{
247-
* [key: string]: (a: AnyParameter) => UnknownResult
248-
* }} UnaryOperatorTable
249-
*/
250-
const result = /** @type {UnaryOperatorTable} */ ({
251-
'-': (a) => -(/** @type {EvaluatedResult} */ (
252-
SafeEval.evalAst(a, subs))
253-
),
254-
'!': (a) => !SafeEval.evalAst(a, subs),
255-
'~': (a) => ~(/** @type {EvaluatedResult} */ (
256-
SafeEval.evalAst(a, subs))
257-
),
258-
// eslint-disable-next-line no-implicit-coercion -- API
259-
'+': (a) => +(/** @type {EvaluatedResult} */ (
260-
SafeEval.evalAst(a, subs))
261-
),
262-
typeof: (a) => typeof SafeEval.evalAst(a, subs),
263-
// eslint-disable-next-line no-void -- Ok
264-
void: (a) => void SafeEval.evalAst(a, subs)
265-
})[ast.operator](ast.argument);
266-
return result;
252+
if (!Object.hasOwn(UNOPS, ast.operator)) {
253+
throw new SyntaxError(`Unknown unary operator: ${ast.operator}`);
254+
}
255+
const operand = SafeEval.evalAst(ast.argument, subs);
256+
return UNOPS[ast.operator](operand);
267257
},
268258

269259
/**

0 commit comments

Comments
 (0)