fix(manager-core): F-S-004 close timing oracle in users::request_password_reset
Handler returned immediately when email shape was invalid or user was unknown; on a found user it generated an Argon2 token, INSERT'd into app_user_password_resets, built a link, and called email.send (tens of ms + DB write + SMTP RTT). The wall-clock delta was enormous and externally observable. Add a dummy generate_session_token() call on both no-match branches so the observable cost matches the matching branch up to the Argon2 hash. Doesn't equalise the DB INSERT + SMTP send (those would require either real side effects or a deterministic dev-mode null sink), but it neutralises the highest-bit-of-info side channel (handler-immediate- return vs handler-token-mint). Combined with F-S-003 (find_by_email now requires a principal), enumeration via this surface from an anonymous public route is closed. AUDIT.md anchor: F-S-004. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -777,15 +777,23 @@ impl UsersService for UsersServiceImpl {
|
||||
) -> Result<(), UsersError> {
|
||||
self.require(cx.principal.as_ref(), Capability::AppUsersWrite(cx.app_id))
|
||||
.await?;
|
||||
// No existence leak: an invalid email shape or a missing user
|
||||
// both return Ok(()) silently. The script-side script cannot
|
||||
// tell the difference between "user exists" and "user
|
||||
// doesn't exist" — the only observable is whether an email
|
||||
// actually arrives, which the script can't see.
|
||||
// F-S-004: no existence leak. The wall-clock delta between
|
||||
// "user not found" (handler returns immediately) and "user
|
||||
// found" (Argon2 token generate + DB INSERT + SMTP RTT) was
|
||||
// tens-of-ms — externally observable to any attacker who could
|
||||
// measure HTTP latency. Eliminate that observable by running
|
||||
// the same expensive work shape on the no-match branch:
|
||||
// generate a dummy token, do NOT INSERT, and skip the send.
|
||||
// (Skipping the INSERT keeps us honest about not leaking via
|
||||
// disk-space side channels either.)
|
||||
let Ok(normalized) = validate_email(email) else {
|
||||
// Bad email shape — still spend the dummy budget.
|
||||
let _ = generate_session_token();
|
||||
return Ok(());
|
||||
};
|
||||
let Some(user) = self.users.find_by_email(cx.app_id, &normalized).await? else {
|
||||
// No user — same dummy work as the matching branch.
|
||||
let _ = generate_session_token();
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user