feat(auth): server-side HIBP backstop on register, password reset, and password change #282

Merged
YousifShkara merged 1 commit from fix/BUNYIP-253-server-hibp-backstop into main 2026-06-30 06:42:20 +02:00
Owner

BUNYIP-253: BUNYIP-240 shipped a client-side HIBP k-anonymity breach check on /register and /reset-password. The check is enforced by the SPA only; any non-browser POST (curl, automation, an XSS-controlled fetch from a malicious origin) bypasses it and lands a breached password in the DB. The audit named this high-severity.

New crates/bunyip-domain/src/services/password_breach.rs mirrors the client-side script: SHA-1 in-process, send only the first 5 hex chars to https://api.pwnedpasswords.com/range/{prefix}, scan the response for the 35-char suffix. The plaintext password never leaves the server.

Wired into AuthService::register, AuthService::complete_password_reset, and AuthService::change_password after validate_strength and before persisting. The change-password path was not in the audit's named list but shares the same shape and would otherwise be the next bypass vector.

Failure mode: fail-open. An HIBP outage / 5xx / parse failure resolves to Unknown -> false and the registration proceeds, logged at warn. A network blip should not lock new users out; the BUNYIP-204 / server-side validate_strength + the BUNYIP-240 client-side check both stay in force as defense in depth.

Privacy: the per-call reqwest client refuses redirects so a 302 from api.pwnedpasswords.com cannot leak the SHA-1 prefix to a non-HIBP host. The 3s timeout keeps a slow upstream from holding the signup actor indefinitely.

New sha1 = "0.10" dep on bunyip-domain (HIBP uses SHA-1, the documented hash for the Pwned Passwords range query; not used for any other purpose).

#BUNYIP-253

BUNYIP-253: BUNYIP-240 shipped a client-side HIBP k-anonymity breach check on `/register` and `/reset-password`. The check is enforced by the SPA only; any non-browser POST (curl, automation, an XSS-controlled fetch from a malicious origin) bypasses it and lands a breached password in the DB. The audit named this high-severity. New `crates/bunyip-domain/src/services/password_breach.rs` mirrors the client-side script: SHA-1 in-process, send only the first 5 hex chars to `https://api.pwnedpasswords.com/range/{prefix}`, scan the response for the 35-char suffix. The plaintext password never leaves the server. Wired into `AuthService::register`, `AuthService::complete_password_reset`, and `AuthService::change_password` after `validate_strength` and before persisting. The change-password path was not in the audit's named list but shares the same shape and would otherwise be the next bypass vector. Failure mode: fail-open. An HIBP outage / 5xx / parse failure resolves to `Unknown -> false` and the registration proceeds, logged at warn. A network blip should not lock new users out; the BUNYIP-204 / server-side `validate_strength` + the BUNYIP-240 client-side check both stay in force as defense in depth. Privacy: the per-call reqwest client refuses redirects so a 302 from `api.pwnedpasswords.com` cannot leak the SHA-1 prefix to a non-HIBP host. The 3s timeout keeps a slow upstream from holding the signup actor indefinitely. New `sha1 = "0.10"` dep on bunyip-domain (HIBP uses SHA-1, the documented hash for the Pwned Passwords range query; not used for any other purpose). #BUNYIP-253
feat(auth): server-side HIBP backstop on register, password reset, and password change
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 21m28s
E2E / Playwright against deployment (pull_request) Successful in 1m4s
Create release / Create release from merged PR (pull_request) Has been skipped
97a7f2fca9
BUNYIP-253: BUNYIP-240 shipped a client-side HIBP k-anonymity breach check on `/register` and `/reset-password`. The check is enforced by the SPA only; any non-browser POST (curl, automation, an XSS-controlled fetch from a malicious origin) bypasses it and lands a breached password in the DB. The audit named this high-severity.

New `crates/bunyip-domain/src/services/password_breach.rs` mirrors the client-side script: SHA-1 in-process, send only the first 5 hex chars to `https://api.pwnedpasswords.com/range/{prefix}`, scan the response for the 35-char suffix. The plaintext password never leaves the server.

Wired into `AuthService::register`, `AuthService::complete_password_reset`, and `AuthService::change_password` after `validate_strength` and before persisting. The change-password path was not in the audit's named list but shares the same shape and would otherwise be the next bypass vector.

Failure mode: fail-open. An HIBP outage / 5xx / parse failure resolves to `Unknown -> false` and the registration proceeds, logged at warn. A network blip should not lock new users out; the BUNYIP-204 / server-side `validate_strength` + the BUNYIP-240 client-side check both stay in force as defense in depth.

Privacy: the per-call reqwest client refuses redirects so a 302 from `api.pwnedpasswords.com` cannot leak the SHA-1 prefix to a non-HIBP host. The 3s timeout keeps a slow upstream from holding the signup actor indefinitely.

New `sha1 = "0.10"` dep on bunyip-domain (HIBP uses SHA-1, the documented hash for the Pwned Passwords range query; not used for any other purpose).

#BUNYIP-253
YousifShkara deleted branch fix/BUNYIP-253-server-hibp-backstop 2026-06-30 06:42:20 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!282
No description provided.