fix(security): hash password on user update; remove broken XSRF middleware#466
Conversation
β¦eware Critical: updateUser was storing plaintext passwords when PATCH /users/:id received a password field. Now hashes via createHash() before DB write, matching the pattern in createUser and resetPasswordWithToken. Critical: XSRF middleware was tautological β it read the token from cookies twice instead of checking request headers, making it ineffective against CSRF attacks. Removed the entire XSRF system (middleware, utility, cookies) since all auth cookies already use sameSite: strict, which provides CSRF protection. Files removed: xsrfTokens.ts, xsrfToken.ts, xsrfTokens.test.ts Config cleanup: EXPIRATION_XSRF_TOKEN removed from required env vars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9ab2fab to
a7a1c8d
Compare
Code Review β PR #466: hash password on user update; remove broken XSRF middlewareOverall this is a solid set of critical security fixes. A few observations below. β What's good
The fix is correct and the reordering is an improvement:
XSRF removal The existing implementation was truly broken. In // Line 34 β reads from cookies
let xsrfToken: string = req.cookies['xsrfToken']
// Line 40 β reads from cookies AGAIN (should have been req.headers['xsrf-token'])
const requestHeadersXsrfToken: string = req.cookies?.['xsrfToken']The "header check" was comparing the cookie to itself, providing zero CSRF protection. Since all auth cookies already use π‘ Issues / Suggestions1. The Authentication Flow section still says:
This should be updated to reflect that CSRF is handled via 2. The diff changes 3. Confirmed no remaining usages β this is clean. π΄ No blockersThe critical password-hashing fix is correct and should be merged. The XSRF removal is well-reasoned and backed by test cleanup. Recommend updating |
Code Review β PR #466: hash password on user update; remove broken XSRF middlewareSummaryThis is a solid security fix. Both changes address real vulnerabilities. Notes below are mostly minor observations and suggestions. β
|
Code Review β PR #466: hash password on user update; remove broken XSRF middlewareOverallGood security fixes. The changes are focused, well-motivated, and the cleanup is thorough. A few things worth flagging.
|
| Area | Status |
|---|---|
| Password hashing bug fix | β Correct |
| XSRF removal rationale | β Sound (caveat on non-browser clients) |
| Code cleanup completeness | β Thorough |
| Test coverage | β Updated correctly |
Verdict: Approve with the caveat that sameSite: strict assumptions are documented if non-browser clients are in scope.
π€ Reviewed with Claude Code
Code Review β PR #466: hash password on user update; remove broken XSRF middlewareOverall this is a solid set of critical fixes. A few observations below.
|
Codecov Reportβ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #466 +/- ##
==========================================
+ Coverage 83.34% 83.71% +0.37%
==========================================
Files 111 109 -2
Lines 1837 1781 -56
Branches 305 295 -10
==========================================
- Hits 1531 1491 -40
+ Misses 198 190 -8
+ Partials 108 100 -8 β View full report in Codecov by Sentry. π New features to boost your workflow:
|
Code Review β PR #466Overall: Approve with minor notes. Both fixes are correct and well-reasoned. The XSRF removal is particularly well-justified.
|
Code Review - PR 466: hash password on user update; remove broken XSRF middlewareOverall this is a well-targeted critical fix. Strengthssrc/services/user/updateUser.ts
XSRF removal
NotesEmpty-string password edge case (updateUser.ts): The password param is typed as string or null or undefined. An empty string is falsy so it will not be hashed and the spread skips it. Worth confirming that PATCH /users/:id with password set to empty string returns AT_LEAST_ONE_PROPERTY_REQUIRED (or that the field is stripped upstream). The safe outcome here is the existing password remains unchanged. Skipped logout route test: describe.skip was already there before this PR, but with logout logic changing substantially across this stack it would be good to track un-skipping it as a follow-up. EXPIRATION_XSRF_TOKEN env var: Removing from requiredEnvVars is correct. A brief release note so existing users know they can remove it from their .env would be helpful. VerdictThe critical password-hashing fix is correct and the XSRF removal is well-reasoned. Approved with the minor notes above for awareness. |
Code Review: PR 466 - hash password on update; remove broken XSRF middlewareCritical: Password hashing fixThe fix is correct and the order of operations is right. Moving isStrongPassword validation before createHash() is the proper sequence - validate first, hash only on success. The spread pattern correctly skips the password field when none was provided. Edge case working as intended: an empty-string password skips both validation and hashing so the field is omitted - no silent plaintext write. Critical: XSRF removalWell-justified. The middleware had two compounding bugs: (1) requestHeadersXsrfToken read from req.cookies again instead of req.headers, making it a cookie-vs-cookie comparison rather than a double-submit check; (2) the XSRF cookie was httpOnly: true, so JavaScript could never read it to inject it into a request header - making a correct double-submit flow literally impossible. Relying on sameSite: strict is a valid standard alternative for CSRF defence. Worth a brief docs note that cookie-based auth only works correctly from same-site browser contexts. Test hygieneTest files removed alongside the implementation - no orphaned tests. src/index.test.ts cleaned up to remove the redundant CSRF header injection. The magicLoginWithToken and logout test expectations updated in sync with the implementation. Minor nitThe old code ran the strong-password check after building updatedUserProperties. The new code validates before hashing - correct reorder. Overall: both fixes are high-quality and well-reasoned. LGTM pending CI green. |
Code Review -- PR 466, 467, 468 (stacked)Reviewed all three PRs together. Overall a solid batch of security fixes. A few things worth addressing before merge. PR 466 -- hash password on update; remove XSRF middlewarePassword hashing in updateUser Fix is correct and the ordering is right: validate raw password strength first, then hash, then conditionally spread into the update object. Matches the pattern in createUser and resetPasswordWithToken. Minor nit: using null as the no-password sentinel means the spread relies on truthiness of the hash. Switching to undefined and checking passwordHash !== undefined would be more explicit and would make silent no-ops impossible. Not a blocker. XSRF middleware removal The bug is real. The middleware read req.cookies['xsrfToken'] twice -- once to obtain the token, then again as the so-called header check -- comparing the cookie value to itself. Since the cookie was httpOnly, client JS could never have put it in a request header anyway. Tautological check correctly removed. env.example needs updating EXPIRATION_XSRF_TOKEN=1w is still present in .env.example. config.ts no longer reads or exports this variable so it is now dead configuration. Should be removed to avoid confusion when bootstrapping a new environment. PR 467 -- rate limit register; remove RESIDENT_TOKEN cookieRegister rate limiting Correct fix. Using rateLimitTenPerTenMins (10 req / 10 min) matches login, magic-login, and reset-password. Good consistency. logout.ts -- error message accuracy The error thrown when refreshTokenId is missing is MESSAGES.MISSING_USER_ID ('User ID is missing.'), but what is actually missing at that point is the refresh token, not the user ID. MESSAGES.REFRESH_TOKEN_REQUIRED would be more accurate for the first guard. logout.ts -- test gap The updated logout.test.ts mocks getToken to always return { userId: '123' }, but there is no test for the case where getToken returns null or returns a record with a falsy userId. Both branches now exist in production code and should be covered. refreshToken.ts -- conditional delete is a net improvement The old code deleted all tokens unconditionally using userId from the RESIDENT_TOKEN cookie. An attacker with a valid RESIDENT_TOKEN but a crafted refreshToken could have wiped all of a user's sessions. The new code derives userId from the DB record, preventing that. Good change. Removed cross-check token.userId !== userId This check was only meaningful when userId came from a separate client-supplied cookie. Since userId is now derived from the DB record the check was redundant and is correctly removed. PR 468 -- explicit JWT algorithm; body size limitJWT algorithm specification Specifying algorithm HS256 on sign and algorithms ['HS256'] on verify closes the algorithm confusion attack surface including alg:none. Clean focused change. Body size limit express.json limit 10kb is a sensible default for an auth API. Routes that need larger payloads should use a route-scoped parser with a higher limit. Summary
The critical and high items are handled well. The three low-severity items are small changes worth addressing before merge. |
Code ReviewSummaryThis is a solid security fix. Both changes are well-motivated and correctly implemented.
|
β¦ AGENTS.md - updateUser: use undefined (not null) for missing passwordHash β idiomatic TS - login: document that sameSite:strict provides CSRF protection - AGENTS.md: update auth flow to reflect XSRF removal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code Review:
|
Code Review: PR 466 - hash password on update + remove broken XSRF middlewareOverall: ApproveTwo solid security fixes. See notes below. What is goodPassword hashing in updateUser is the critical fix. Plaintext password storage on PATCH /users/:id was a serious vulnerability. The change correctly applies createHash() and mirrors the pattern in createUser and resetPasswordWithToken. Validation-before-hashing order β moving isStrongPassword above the createHash call is correct: fail fast before the expensive bcrypt operation. XSRF removal is well-justified. The middleware had a fundamental logical defect: it compared req.cookies['xsrfToken'] to itself (read twice from cookies) instead of comparing the cookie value to the request header. Because the cookie was httpOnly, client JS could never read and re-send it, making the double-submit pattern impossible. sameSite=strict on all auth cookies already provides CSRF protection for same-origin APIs, so removing this broken layer is the right call. The comment added to login.ts explaining the decision is a good addition. Issues to fix before merging1. .env.example still has EXPIRATION_XSRF_TOKEN=1w This will confuse anyone bootstrapping the project after this merge. Please remove the line in this PR. 2. Dead message constants MESSAGES.TOKEN_USER_INVALID and MESSAGES.REFRESH_TOKEN_COUNTERPART_REQUIRED remain in src/constants/messages.ts and will be unreferenced once PR 467 is merged. One of the two stacked PRs should clean them up. Minor notes
|
Code ReviewOverall this is an excellent and necessary security fix. The critical issues are addressed cleanly. What is well donePassword hashing fix (updateUser.ts) - The critical bug (plaintext password storage) is correctly resolved. The password strength validation is now done before hashing, which is the right order: fail fast on weak passwords before spending bcrypt cycles. XSRF removal - The broken middleware was genuinely tautological: it read req.cookies['xsrfToken'] twice instead of checking the request header, and the cookie was httpOnly: true so client-side JS could never read it to set the header anyway. Documenting the sameSite: strict guarantee in the login comment is a good replacement callout. Test cleanup - All XSRF-related assertions and imports are cleanly removed with no orphaned mocks or dead test code. Bugs / Issues.env.example still references EXPIRATION_XSRF_TOKEN (line 22) This env var is no longer read by config.ts, but anyone setting up a fresh environment from .env.example will still see it. Should be removed to avoid confusion. Minor notes
Test coverageThe existing XSRF test file is fully deleted rather than updated - appropriate since the middleware is gone. The updateUser password hashing fix would benefit from a unit test asserting the stored value starts with the bcrypt prefix ( Verdict: Approve once .env.example is updated to remove EXPIRATION_XSRF_TOKEN. The core security fix is solid. |
Code Review β PR #466: hash password on user update; remove broken XSRF middlewareOverall this is a solid, well-targeted fix. Both changes address real bugs and are implemented cleanly. β updateUser β password hashing (Critical fix)The fix is correct and the refactor is an improvement. The reordering (validate β hash β spread) is the right sequence: fail fast on a weak password before doing the expensive bcrypt operation. This matches the pattern already used in createUser and resetPasswordWithToken. Before: Password was spread into the update object in plaintext, with the strength check happening after construction β wasteful. After:
β Removal of the XSRF middlewareThe original xsrfTokens.ts had two fatal bugs:
sameSite: strict is the correct CSRF defence for a cookie-based API consumed by first-party clients. Removing the broken middleware (rather than patching it) is the right call. Minor observations
Test coverageTests updated correctly throughout β XSRF assertions removed from logout, magicLoginWithToken, refreshToken, and index tests. The config.test.ts cleanup is tidy. Verdict: Approve β fixes are correct, minimal, and well-tested. |
Code Review: fix(security): hash password on user update; remove broken XSRF middlewareGood security fixes overall. Notes below. StrengthsPassword hashing fix ( The critical plaintext-password bug is correctly fixed. Moving the XSRF removal The middleware was genuinely broken β it read from Issues1. Test does not verify hashing actually happens (medium)
Suggested addition in the passing test: import { createHash } from '../../utils/crypt'
// inside the test:
expect(createHash).toHaveBeenCalledWith('StrongP@ssw0rd')2. Version mismatch between
3. The env var is removed from Notes
|
Code ReviewStrengthsPassword hashing fix (updateUser.ts): Correct and well-executed. The reordering β validate strength, hash, then store β is cleaner than before. Previously the plaintext password was spread into updatedUserProperties and written directly to the DB; the strength check ran before the write but the hash step was entirely absent. Using the existing createHash() utility keeps this consistent with createUser and resetPasswordWithToken. XSRF middleware removal: The original xsrfTokens.ts was definitively broken β requestHeadersXsrfToken read from req.cookies rather than req.headers, so it compared the cookie to itself and never validated an incoming CSRF header token. Removing it is the right call. sameSite: strict on HttpOnly cookies is a well-accepted CSRF mitigation for same-site browser clients, and AGENTS.md is correctly updated to reflect this. All test files are updated consistently. IssuesVersion mismatch β package.json vs package-lock.json: package.json is bumped to 0.3.8 but package-lock.json is bumped to 0.3.7. These must match. Run npm install (without adding packages) to sync the lock file, or use npm version patch which updates both atomically. Non-blocking observations
Overall: solid critical security fix. The plaintext password storage bug was high severity. Approved pending the version bump fix. |
Summary
updateUserservice was storing passwords in plaintext whenPATCH /users/:idreceived apasswordfield. Now hashes viacreateHash()before the DB write, matching the pattern increateUserandresetPasswordWithToken.httpOnlyso JS couldn't read it anyway. Removed the entire XSRF system (middleware, utility, cookies set on login/refresh/magic-login/google).sameSite: stricton all auth cookies already provides CSRF protection.Files removed
src/middleware/auth/xsrfTokens.tssrc/middleware/util/xsrfToken.tssrc/middleware/auth/xsrfTokens.test.tsTest plan
npm run lint && npm testgreenPATCH /users/:idwith apasswordfield β verify bcrypt hash stored in DB ($2b$prefix)sameSite: stricthandles CSRF)π€ Generated with Claude Code