Fix: Server-side template injection: GET /profile passes the user-controlled username to eval(), enabling remote code execution - #363
Conversation
…RCE) Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Wes Convery <2wconvery@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| if (username?.match(/#{(.*)}/) !== null && utils.isChallengeEnabled(challenges.usernameXssChallenge)) { | ||
| req.app.locals.abused_ssti_bug = true |
There was a problem hiding this comment.
🟡 SSTi challenge becomes unsolvable
Without the abused_ssti_bug assignment, the documented exploit never unlocks sstiChallenge. The server-side verifier requires that flag, leaving fresh instances with an unsolvable challenge.
Learn more
The SSTi challenge remains in the challenge catalog and its verifier still requires req.app.locals.abused_ssti_bug === true. This change removes the only assignment that can make that condition true. The existing Cypress scenario also depends on this profile flow, so it can no longer solve the challenge.
Example: On a fresh non-Docker instance, a player stores the documented #{...} username, loads /profile, and requests the server-side verification URL. The flag remains false, the verifier calls next(), and the SSTi challenge stays unsolved.
Recommended fix: If hardened deployments must remove this training path, disable or remove sstiChallenge consistently from the catalog, verifier, configuration, and Cypress coverage. Otherwise, replace the removed path with a maintainer-approved challenge mechanism that still sets the verifier state without restoring the unsafe evaluation.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Intentional: this PR removes the SSTi training path, so sstiChallenge becomes unsolvable by design in a hardened deployment (noted in the description). Retiring the challenge from the catalog/verifier/Cypress is a maintainer decision per AGENTS.md and is left out of this security fix.
| div.s12.m12.l6 | ||
| img.img-rounded(src=profileImage, alt='profile picture', width='90%', height='236', style='margin-right: 5%; margin-left: 5%;') | ||
| p(style='margin-top: 8px; color: _textColor_; text-align: center;') _username_ | ||
| p(style='margin-top: 8px; color: _textColor_; text-align: center;')= username |
There was a problem hiding this comment.
🟡 CSP Bypass awards false completion
With the matching CSP URL and script username, p= username escapes the payload while the solve check awards completion. Players receive credit without executing the required alert.
Learn more
The profile model's legacy sanitizer can store the exact script string expected by the route. Escaped Pug interpolation converts its angle brackets to HTML entities, so the browser never creates a script element. The unchanged solve check only inspects the stored strings and therefore still records success.
Example: A player sets the profile image to https://a.png; script-src 'unsafe-inline' and the sanitized username to <script>alert(xss)</script>. Loading /profile renders escaped text, but usernameXssChallenge is marked solved.
Recommended fix: Keep escaped interpolation for the hardened profile. Then retire or disable usernameXssChallenge and remove its obsolete solve check and E2E scenario. If the challenge must remain, redesign it as a maintainer-approved isolated training path whose completion verifies actual challenge behavior.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Acknowledged — the usernameXssChallenge solve check only inspects stored strings, so it can now score on inert escaped text. Adjusting/retiring that challenge is out of scope here and needs maintainer sign-off; the hardened rendering is kept as recommended.
| } else { | ||
| username = '\\' + username | ||
| } | ||
| const username = user.username |
There was a problem hiding this comment.
There was a problem hiding this comment.
Correct — the Cypress scenarios for SSTi and CSP Bypass depend on the removed behavior and will need updating if this hardening is adopted. Intentionally not modifying tests in this PR; flagged for maintainers.
Description
Finding: Server-side template injection: GET /profile passes the user-controlled username to eval(), enabling remote code execution
Repo: COG-GTM/juice-shop
Severity: CRITICAL (VULNERABILITY)
routes/userProfile.tsmatched the stored username against/#{(.*)}/and passed the captured content toeval()server-side, then spliced the result into the Pug template source via_username_string replacement. Any registered user sets their own username throughPOST /profile, soGET /profilegave authenticated users arbitrary code execution in the Node.js process.Fix approach: remove the
eval()branch and the_username_source-replacement entirely, and render the username through Pug's escaped interpolation (p= username) so it is treated as data, not template/code.Note: this removes the training path behind
sstiChallenge(req.app.locals.abused_ssti_bugis now never set), which is intentional for a hardened deployment.Supersedes #362 (closed: wrong base branch / missing DCO sign-off).
Verified:
eslint routes/userProfile.ts,tsc --noEmit, andmocha test/server(252 passing).Resolved or fixed issue: none
AI Tool Disclosure
DevinClaude (via Devin)Daily security sweep: find and remediate substantiated vulnerabilities in COG-GTM/juice-shopAffirmation
Link to Devin session: https://app.devin.ai/sessions/7c3e2a1297704d799ec718bd3411e45e
Open in Devin Desktop: https://app.devin.ai/desktop/session/7c3e2a1297704d799ec718bd3411e45e?variant=devin
Requested by: @WesternConcrete