agentggagentgg
Back to all findings
HIGHconfirmedrcerceac4e2f90b3de

eval() of substring from user-controlled username enables RCE/SSTI

getUserProfile evaluates a substring of the persisted username as JavaScript via eval(), and the username originates from user input through the profile update endpoint — yielding full remote code execution on the server.

Fileroutes/userProfile.ts
Lines5266
Confidence
95%
File statusvalidated
Details

In routes/userProfile.ts the username read from the database is matched against a #{...} pattern and the inner expression is passed straight to eval:

let username = user.username
if (username?.match(/#{(.*)}/) !== null && utils.isChallengeEnabled(challenges.usernameXssChallenge)) {
  req.app.locals.abused_ssti_bug = true
  const code = username?.substring(2, username.length - 1)
  try {
    if (!code) {
      throw new Error('Username is null')
    }
    username = eval(code) // eslint-disable-line no-eval
  } catch (err) {
    username = '\\' + username
  }
} else {
  username = '\\' + username
}

user.username is stored on the User model and is updated from req.body.username in routes/updateUserProfile.ts (line 35: await user.update({ username: req.body.username })). The update endpoint performs no validation on the username string. An attacker who is logged in can therefore set their username to a value such as #{require('child_process').execSync('id')}, then visit /profile, causing eval() to execute arbitrary JavaScript in the Node.js process. This grants full server-side code execution under the privileges of the Juice Shop process.

Proof of concept
  1. Authenticate as any user.
  2. POST /profile with form body username=#{global.process.mainModule.require('child_process').execSync('id').toString()} (the username challenge flag must be enabled, which it is by default).
  3. GET /profile — the server runs eval("global.process.mainModule.require('child_process').execSync('id').toString()"), executing arbitrary OS commands.
Impact

Authenticated remote code execution in the application server process. An attacker can execute arbitrary shell commands, read filesystem secrets (including the JWT signing key in encryptionkeys/), pivot to internal systems, and fully compromise the host. Only basic authentication (any user account) is required.

Validation
confirmed

Line 60 username = eval(code) directly evaluates a substring of user.username, which is persisted from unvalidated req.body.username in the profile update route. An authenticated attacker setting their username to #{require('child_process').execSync('id')} triggers RCE on the next /profile visit, gated only by a regex match and the usernameXssChallenge flag (enabled by default). Scope explicitly forbids dismissing Juice Shop findings due to its training nature, so this is a real, exploitable RCE.

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 eval(code) in routes/userProfile.ts line 61 executes a substring of user.username, which is attacker-controlled via the unvalidated req.body.username update at routes/updateUserProfile.ts line 35, reachable remotely over HTTP (AV:N). Exploitation requires only a logged-in session check via security.authenticatedUsers.get(req.cookies.token) — any user account suffices (PR:L) — and the attacker triggers their own profile fetch, so no victim is needed (UI:N). The conditions are deterministic (challenge flag is enabled by default, regex/substring always reaches eval) so AC:L. Arbitrary Node.js code execution in the Juice Shop process yields full read/write of the filesystem and the ability to crash the server (C/I/A:H); the impact remains within the application's own security authority, so Scope is Unchanged.

References