RCE via vm.runInContext evaluating user-controlled orderLinesData
b2bOrder evaluates request-supplied orderLinesData inside Node's vm context via notevil's safeEval; both the vm module and notevil have documented sandbox-escape paths leading to remote code execution.
routes/b2bOrder.ts accepts body.orderLinesData directly from the request and pipes it through a Node vm context:
const orderLinesData = body.orderLinesData || ''
const sandbox = { safeEval, orderLinesData }
vm.createContext(sandbox)
vm.runInContext('safeEval(orderLinesData)', sandbox, { timeout: 2000 })
Two independent issues here:
- Node
vmis not a security sandbox. The Node docs explicitly warn that thevmmodule does not provide a security boundary; code executed inside can reach host globals via the prototype chain. The 2-secondtimeoutis a DoS guard, not an isolation guard, and there is no memory budget at all. - The interpreter inside the sandbox is
notevil'ssafeEval. notevil parses the JS AST and walks it in JavaScript, but it has a long history of bypasses (constructor-property tricks, prototype pollution, native function leakage). WithorderLinesDatafully attacker-controlled, the request body is the input to that evaluator.
The solveIf(challenges.rceChallenge, ...) and solveIf(challenges.rceOccupyChallenge, ...) callbacks confirm that even the application's own challenge scoring treats this endpoint as the RCE / RCE-DoS sink.
POST /b2b/v2/orders with body { "orderLinesData": "(function(){while(true){}})()" } triggers the rceOccupyChallenge 503 path (vm timeout). A successful sandbox-escape payload that hits the prototype chain of one of the safeEval-exposed objects gets code execution in the Node process; juice-shop's own writeups demonstrate this with payloads such as constructor-chain attacks against notevil to invoke process.mainModule.require('child_process').execSync(...).
Unauthenticated-to-authenticated B2B caller (route is behind security.isAuthorized() on /b2b/v2) can run arbitrary JavaScript in the server process or hang it. Successful escape yields full code execution as the juice-shop user — read/write filesystem, exfiltrate the database, pivot.
The code at lines 20-23 literally takes body.orderLinesData from the request and passes it through vm.runInContext('safeEval(orderLinesData)', sandbox, { timeout: 2000 }). Node's vm is explicitly not a security boundary, and notevil's safeEval has documented escape paths (constructor-chain bypasses) leading to process.mainModule.require('child_process'). The application's own rceChallenge/rceOccupyChallenge solve callbacks corroborate this endpoint as the RCE sink, and the scope directs us to treat the app as production. Exploit chain is reachable from an authenticated B2B caller via untrusted POST body input.
The sink is reached via an HTTP POST to /b2b/v2/orders (AV:N) with body.orderLinesData flowing directly into vm.runInContext('safeEval(orderLinesData)', sandbox, ...); per the finding the route is mounted behind security.isAuthorized(), so any authenticated B2B caller suffices (PR:L), with no victim interaction (UI:N). The DoS path (infinite loop → 2s timeout → 503) is trivial, and notevil has documented constructor/prototype-chain bypasses that yield process.mainModule.require('child_process').execSync(...), so AC:L is appropriate. Successful escape gives arbitrary code execution as the juice-shop process — full read/write of files and database and the ability to hang the server — giving C:H/I:H/A:H within the Node application; impact stays inside that same security authority, so Scope is Unchanged.