agentggagentgg
Back to all findings
HIGHconfirmedrcercea65a0378566a

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.

Fileroutes/b2bOrder.ts
Lines1735
Confidence
95%
File statusvalidated
Details

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:

  1. Node vm is not a security sandbox. The Node docs explicitly warn that the vm module does not provide a security boundary; code executed inside can reach host globals via the prototype chain. The 2-second timeout is a DoS guard, not an isolation guard, and there is no memory budget at all.
  2. The interpreter inside the sandbox is notevil's safeEval. 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). With orderLinesData fully 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.

Proof of concept

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(...).

Impact

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.

Validation
confirmed

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.

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Base score: 8.8 · HIGH

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.

References