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.
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.
- Authenticate as any user.
- 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). - GET /profile — the server runs
eval("global.process.mainModule.require('child_process').execSync('id').toString()"), executing arbitrary OS commands.
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.
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.
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.
- CWE-94
- CWE-95
- OWASP A03:2021 Injection