Skip to content

fix: logout CSRF (A-1) and login user-enumeration timing (A-2)#76

Merged
andregoepel merged 1 commit into
mainfrom
fix/login-csrf-and-timing
Jul 4, 2026
Merged

fix: logout CSRF (A-1) and login user-enumeration timing (A-2)#76
andregoepel merged 1 commit into
mainfrom
fix/login-csrf-and-timing

Conversation

@andregoepel

Copy link
Copy Markdown
Owner

Two low-severity hardenings from the security audit.

A-1 — Logout CSRF on /Account/SignOutAndRedirect

/Account/SignOutAndRedirect is a MapGet that calls SignOutAsync(). A GET with a side effect is triggerable cross-site (<img src>, prefetch, a crafted link) with no antiforgery token, so any page the victim visits can silently sign them out.

It can't simply become a POST: the endpoint exists because an interactive Blazor circuit cannot write the sign-out cookie itself — DeletePersonalData.razor force-navigates here after erasing the account. So instead this adds a same-origin gate using Sec-Fetch-Site (the same mechanism CookieLoginMiddleware already uses for the login handoff): a cross-site request is rejected and redirected home without signing out, while the legitimate same-origin navigation still works. Fails open only for legacy clients that don't send Sec-Fetch-Site (acceptable for a logout-CSRF nuisance).

A-2 — User enumeration via login timing

LoginForm returned immediately when FindByEmailAsync found no account, skipping the password hash that a real attempt runs via CheckPasswordAsync. The measurable timing delta lets an attacker distinguish registered from unregistered emails (CWE-208) even though the response message is already uniform.

Fix: on the not-found path, perform a throwaway PasswordHasher.HashPassword(new User(), password) so both paths pay ~one KDF pass. (HashPassword avoids the hash-format concerns of VerifyHashedPassword against a canned hash.) Per-account lockout and host rate limiting remain the primary throttles; this closes the timing side channel.

Tests

  • IPasswordHasher<User> registered in the LoginForm bUnit host; the 5 LoginForm tests and all 68 Blazor tests pass.
  • Build 0 warnings; CSharpier clean. No behavior change to the success or wrong-password paths.

Both were flagged Low in the audit and excluded from the earlier Medium+ issue batch, so there are no tracking issues to close.

…ration

Two low-severity hardenings from the security audit:

A-1 (logout CSRF): /Account/SignOutAndRedirect is a GET that signs out. It
exists because an interactive Blazor circuit cannot write the sign-out cookie
(the DeletePersonalData flow force-navigates to it), so it can't simply become a
POST. A cross-site page could force a sign-out via <img>/link/prefetch. Reject
cross-site requests via Sec-Fetch-Site (mirroring the login-handoff same-origin
check); the legitimate same-origin navigation still works.

A-2 (user enumeration): the login form returned immediately when the email did
not match any account, skipping the password hash that a real attempt performs —
a timing oracle distinguishing registered from unregistered emails. Perform a
throwaway password hash on the not-found path so both cost the same (CWE-208).
Register IPasswordHasher<User> in the LoginForm bUnit test host.
@andregoepel
andregoepel merged commit 9c24506 into main Jul 4, 2026
3 checks passed
@andregoepel
andregoepel deleted the fix/login-csrf-and-timing branch July 6, 2026 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant