Skip to content

Commit 63993cb

Browse files
committed
fix(security): vulnerability with safe script
1 parent 3293210 commit 63993cb

2 files changed

Lines changed: 81 additions & 2 deletions

File tree

src/Safe-Script.js

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

43+
// Every function-constructor variant, along with the invocation helpers which
44+
// could otherwise reach them indirectly, e.g., `Function.call(0, 'code')()`
45+
/** @type {WeakSet<object>} */
46+
const BLOCKED_FUNCTIONS = new WeakSet([
47+
Function,
48+
// eslint-disable-next-line no-empty-function -- Only need the constructor
49+
function *() {}.constructor,
50+
// eslint-disable-next-line no-empty-function -- Only need the constructor
51+
async function () {}.constructor,
52+
// eslint-disable-next-line no-empty-function -- Only need the constructor
53+
async function *() {}.constructor,
54+
Function.prototype.call,
55+
Function.prototype.apply,
56+
Function.prototype.bind,
57+
Reflect.apply,
58+
Reflect.construct
59+
]);
60+
61+
/**
62+
* @param {UnknownResult} value
63+
* @returns {boolean}
64+
*/
65+
const isBlockedFunction = (value) => {
66+
return typeof value === 'function' && BLOCKED_FUNCTIONS.has(value);
67+
};
68+
4369
/**
4470
* @typedef {Record<
4571
* string,
@@ -256,7 +282,10 @@ const SafeEval = {
256282
);
257283
}
258284
const result = /** @type {Record<string, UnknownResult>} */ (obj)[prop];
259-
if (typeof result === 'function' && result !== Function) {
285+
if (isBlockedFunction(result)) {
286+
throw new TypeError('Function constructor is disabled');
287+
}
288+
if (typeof result === 'function') {
260289
return result.bind(obj); // arrow functions aren't affected by bind.
261290
}
262291
return result;
@@ -297,7 +326,10 @@ const SafeEval = {
297326
evalCallExpression (ast, subs) {
298327
const args = ast.arguments.map((arg) => SafeEval.evalAst(arg, subs));
299328
const func = SafeEval.evalAst(ast.callee, subs);
300-
if (func === Function) {
329+
if (
330+
isBlockedFunction(func) ||
331+
args.some((arg) => isBlockedFunction(arg))
332+
) {
301333
throw new Error('Function constructor is disabled');
302334
}
303335
return (/** @type {(...args: AnyParameter[]) => UnknownResult} */ (

test/test.safe-eval.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,53 @@ checkBuiltInVMAndNodeVM(function (vmType, setBuiltInState) {
328328
}, "Function constructor is disabled");
329329
});
330330

331+
it("10.4.1 RCE via call/apply/bind", () => {
332+
// @ts-expect-error VM testing
333+
// eslint-disable-next-line unicorn/no-global-object-property-assignment -- Exploit test
334+
globalThis.TEST_10_4_1_RCE_INDIRECT = 'not exploited';
335+
336+
const payload =
337+
'globalThis.TEST_10_4_1_RCE_INDIRECT="RCE";0';
338+
for (const invocation of [
339+
`constructor.call(0,'${payload}')()`,
340+
`constructor.apply(0,['${payload}'])()`,
341+
`constructor.bind(0)('${payload}')()`
342+
]) {
343+
assert.throws(() => {
344+
const path =
345+
`$..[?(@.constructor[( @.getPrototypeOf(@).${invocation} )])]`;
346+
jsonpath({path, json: {a: {}}});
347+
}, "Function constructor is disabled");
348+
}
349+
350+
assert.equal(
351+
// @ts-expect-error VM testing
352+
globalThis.TEST_10_4_1_RCE_INDIRECT,
353+
'not exploited'
354+
);
355+
});
356+
357+
it("async/generator function constructors blocked", () => {
358+
const fnJson = {
359+
a: {
360+
// eslint-disable-next-line no-empty-function -- Only need the constructor
361+
af: async function () {}.constructor,
362+
// eslint-disable-next-line no-empty-function -- Only need the constructor
363+
gf: function *() {}.constructor,
364+
// eslint-disable-next-line no-empty-function -- Only need the constructor
365+
agf: async function *() {}.constructor
366+
}
367+
};
368+
for (const prop of ['af', 'gf', 'agf']) {
369+
assert.throws(() => {
370+
jsonpath({
371+
json: fnJson,
372+
path: `$[?(@.${prop}('return 1')())]`
373+
});
374+
}, "Function constructor is disabled");
375+
}
376+
});
377+
331378
it("bind() escape guard: function.prototype.constructor blocked", () => {
332379
// Regression: bound functions (with no .prototype) are returned to
333380
// prevent @.f.prototype.constructor → Function constructor escape.

0 commit comments

Comments
 (0)