agentggagentgg
Back to all findings
HIGHconfirmedsstissti347b6665851b

SSTI via username interpolated into Pug template source before compile

User-controlled `username` is string-concatenated into a Pug template source, then `pug.compile(template)` is invoked, allowing server-side template injection (and explicit `eval()` of `#{...}` content).

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

The handler reads views/userProfile.pug from disk and then mutates that source string with template.replace(/_username_/g, username) before calling pug.compile(template) and executing the compiled function with fn(user).

username originates from the persisted UserModel record (stored user-controlled content — users can change their username), so an attacker can place arbitrary Pug syntax (e.g. #{...}, - code, !{...}) into the template source that Pug then compiles and executes server-side. The naive '\\' + username escape only prepends a single backslash and does not neutralize Pug interpolation/code-block syntax embedded later in the string.

Worse, the code already contains an explicit RCE branch:

if (username?.match(/#{(.*)}/) !== null && utils.isChallengeEnabled(challenges.usernameXssChallenge)) {
  req.app.locals.abused_ssti_bug = true
  const code = username?.substring(2, username.length - 1)
  ...
  username = eval(code) // eslint-disable-line no-eval
}

If the stored username matches #{...}, the inner expression is passed straight to eval() in the Node process. Even if that branch is disabled, the fall-through path still concatenates username into the Pug source before compilation, which is the canonical SSTi pattern (template source derived from request/stored input).

This exactly matches the brief's true-positive criteria: (1) pug.compile accepts a template source string, (2) that string contains data transitively controlled by a user (their stored username), and (3) the compiled template is then executed via fn(user).

Proof of concept
  1. Register/update a user with username #{global.process.mainModule.require('child_process').execSync('id').toString()}.
  2. Authenticate and GET the user profile route that invokes getUserProfile().
  3. The handler hits the #{(.*)} branch and calls eval("global.process.mainModule.require('child_process').execSync('id').toString()"), returning the command output as the rendered username. Alternatively, set username to a payload like #{function(){return global.process.mainModule.require('child_process').execSync('id')}()} — once interpolated into the Pug source and compiled, Pug executes it server-side.
Impact

Authenticated users (anyone who can set their own username) achieve arbitrary code execution in the Node.js server process, leading to full host compromise, secret exfiltration, and lateral movement. The eval() branch is gated only by a feature flag (isChallengeEnabled); the underlying compile-of-user-data pattern is exploitable regardless.

Validation
confirmed

The code at line ~67 explicitly calls eval(code) on a substring extracted from user.username whenever the username matches /#{(.*)}/ and the challenge flag is on — this is a direct authenticated-RCE primitive controlled by a user-settable field. Additionally, template.replace(/_username_/g, username) mutates the Pug source before pug.compile(template) runs and fn(user) is executed; the only sanitization is a single leading '\\' + which doesn't neutralize embedded newlines/indented code blocks (- ..., !{...}) injected later in the username. Both the username field (registration/profile update) and authenticated access to getUserProfile() are reachable by any registered user, satisfying the brief's stored-template-source criterion. Scope explicitly says to treat Juice Shop findings as production bugs, so the intentional-vuln status does not downgrade this.

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 reachable over HTTP via the profile route, requiring only a valid session token checked by security.authenticatedUsers.get(req.cookies.token) — so AV:N and PR:L (any registered user who can set their own username). No victim action is needed: the attacker sets their own username to a Pug interpolation payload and then requests their own profile, which triggers eval(code) and/or pug.compile(template) on attacker-controlled source, giving arbitrary Node.js code execution in the server process. Exploitation is deterministic with no race or special configuration (the eval branch is gated by a feature flag, but the underlying compile-of-user-data path executes unconditionally), so AC:L. RCE in the application process yields full read/write of application data and trivially can hang/crash the worker, hence C:H/I:H/A:H; the compromise stays within the Node app's security authority so Scope is Unchanged.

References