CSRF: POST /profile authenticates via cookie and mutates username with no CSRF protection
`POST /profile` reads the session purely from `req.cookies.token` (not the Bearer header) and updates the authenticated user's username, while the application registers no CSRF middleware and the `token` cookie is set without `SameSite=strict`.
Registered in server.ts L658:
app.post('/profile', utils.asyncHandler(updateUserProfile()))
No csurf / csrf-csrf / doubleCsrf / lusca.csrf middleware appears anywhere in the codebase (verified with a repo-wide grep). The handler in routes/updateUserProfile.ts reads identity from a cookie:
const loggedInUser = security.authenticatedUsers.get(req.cookies.token)
...
const savedUser = await user.update({ username: req.body.username })
...
res.cookie('token', updatedToken)
The token cookie is set in lib/insecurity.ts (updateAuthenticatedUsers) and here with res.cookie('token', token) — no sameSite option, no httpOnly, no secure. Express defaults give SameSite=Lax, which still allows top-level cross-origin POSTs in some browsers; even where Lax blocks the request, the application's CORS policy (server.ts L188-189 app.options('*', cors()); app.use(cors())) reflects any origin, so an attacker page can submit application/x-www-form-urlencoded and have the browser auto-attach the cookie.
The handler itself contains the giveaway:
challengeUtils.solveIf(challenges.csrfChallenge, () => {
return ((req.headers.origin?.includes('://htmledit.squarefree.com')) ?? ...) && req.body.username !== user.username
})
It explicitly rewards a request originating from htmledit.squarefree.com, confirming this endpoint is exploited by serving a CSRF page from another origin.
A repo-wide review of state-changing endpoints in server.ts shows the rest authenticate via Bearer (security.isAuthorized() uses expressJwt against the Authorization header, and appendUserId/isAccounting read via jwtFrom which only inspects Authorization: Bearer). POST /profile is the outlier that trusts the cookie alone, which makes it a real CSRF sink rather than a Bearer-only API where browsers cannot auto-attach credentials.
Host the following at any attacker-controlled origin and have a logged-in victim visit it:
<form id="f" action="https://victim-juiceshop/profile" method="POST">
<input name="username" value="pwn3d">
</form>
<script>document.getElementById('f').submit();</script>
The browser attaches the token cookie, the handler resolves the victim from req.cookies.token, and UserModel.update({ username: req.body.username }) persists pwn3d as the victim's username. The 302 redirect to /profile completes the navigation.
Any authenticated user visiting an attacker page has their profile mutated. The username setter runs through the (deliberately weak) security.sanitizeLegacy/sanitizeSecure HTML stripper, so this CSRF also chains into the stored-XSS sinks at administration.component.html / search-result.component.ts that render user.username via [innerHTML]. No authentication beyond the victim's existing session is required.
The handler authenticates via security.authenticatedUsers.get(req.cookies.token) (cookie-only, no Bearer), performs a state-changing user.update({ username: req.body.username }), and has no CSRF token validation. The presence of challengeUtils.solveIf(challenges.csrfChallenge, ...) that explicitly checks for an origin/referer of htmledit.squarefree.com is built-in confirmation that this endpoint is intentionally exploitable via cross-origin POST. A simple HTML form auto-submit from an attacker origin will cause the browser to attach the token cookie and persist the attacker-controlled username — the PoC is reachable.
The attacker hosts a remote HTML form (AV:N) and needs no special conditions beyond a victim being logged in (AC:L); the attacker themselves needs no account on the target (PR:N) because the handler reads identity from req.cookies.token, which the browser auto-attaches to the cross-origin POST. A victim must visit the attacker page for the form to auto-submit (UI:R). The state change is confined to the victim's own user record via user.update({ username: req.body.username }), so Scope is Unchanged and Integrity is Low — the attacker can only mutate the username field, not arbitrary data; Confidentiality and Availability are unaffected as nothing is read back or denied. (The mentioned stored-XSS chain via [innerHTML] rendering of username is a separate sink in other components and is not scored here.)
- CWE-352
- OWASP ASVS 4.2.2
- OWASP Top 10 A01:2021