Summary
After validating a password-reset token on the initial GET, the controller redirects to the change-password page with the raw token retained as a query-string parameter. The token is not consumed at this GET, so it stays valid (default 24h) while sitting in a user-visible URL.
Severity: Medium
Affected code
src/main/java/com/digitalsanctuary/spring/user/controller/UserActionController.java:79 — showChangePasswordPage; line ~92 re-adds the raw token to the redirect model, producing Location: /user/forgot-password-change.html?token=<token>.
src/main/java/com/digitalsanctuary/spring/user/service/UserService.java:979 — validatePasswordResetToken (GET) validates/expiry-checks only; it does not consume a valid token.
src/main/java/com/digitalsanctuary/spring/user/service/UserService.java:688 — validateAndConsumePasswordResetToken (atomic single-use consume) runs only later on the POST /user/savePassword.
Details
Because the token is placed into a redirect (redirect: → 302 with a Location query parameter), it lands in the address bar, browser history, and the Referer sent when the change page loads sub-resources. No Referrer-Policy or Cache-Control headers are set on these pages. Since the GET does not consume the token, a leaked-but-unused token remains replayable until the password is submitted or the token expires (passwordResetTokenValidityMinutes default 1440).
Note: the emailed reset link itself already carries the token in a URL, so this redirect propagates token-in-URL rather than introducing it — but it does add a second URL and the associated Referer/history/analytics exposure (CWE-598).
Recommended fix
- On the first GET, consume/exchange the token for short-lived server-side one-purpose state (flash/session attribute) and redirect (
303 See Other) to a clean URL with no token.
- Add
Referrer-Policy: no-referrer and Cache-Control: no-store on the reset pages.
UserActionControllerTest currently asserts the token-in-URL behavior (redirectedUrl(... ?token=...)); update alongside the fix.
Identified during a code-first security assessment of the current main branch.
Summary
After validating a password-reset token on the initial GET, the controller redirects to the change-password page with the raw token retained as a query-string parameter. The token is not consumed at this GET, so it stays valid (default 24h) while sitting in a user-visible URL.
Severity: Medium
Affected code
src/main/java/com/digitalsanctuary/spring/user/controller/UserActionController.java:79—showChangePasswordPage; line ~92 re-adds the raw token to the redirect model, producingLocation: /user/forgot-password-change.html?token=<token>.src/main/java/com/digitalsanctuary/spring/user/service/UserService.java:979—validatePasswordResetToken(GET) validates/expiry-checks only; it does not consume a valid token.src/main/java/com/digitalsanctuary/spring/user/service/UserService.java:688—validateAndConsumePasswordResetToken(atomic single-use consume) runs only later on thePOST /user/savePassword.Details
Because the token is placed into a redirect (
redirect:→ 302 with aLocationquery parameter), it lands in the address bar, browser history, and theReferersent when the change page loads sub-resources. NoReferrer-PolicyorCache-Controlheaders are set on these pages. Since the GET does not consume the token, a leaked-but-unused token remains replayable until the password is submitted or the token expires (passwordResetTokenValidityMinutesdefault 1440).Note: the emailed reset link itself already carries the token in a URL, so this redirect propagates token-in-URL rather than introducing it — but it does add a second URL and the associated
Referer/history/analytics exposure (CWE-598).Recommended fix
303 See Other) to a clean URL with no token.Referrer-Policy: no-referrerandCache-Control: no-storeon the reset pages.UserActionControllerTestcurrently asserts the token-in-URL behavior (redirectedUrl(... ?token=...)); update alongside the fix.Identified during a code-first security assessment of the current
mainbranch.