fix(manager-core): F-S-002 rate-limit users::request_password_reset + send_verification_email
Both methods short-circuit the authz gate when cx.principal == None. An attacker hitting any public route could trigger unbounded outbound email per call to arbitrary or attacker-supplied addresses — exhausting SMTP-relay quota, getting the relay blacklisted, or burning provider credits. Add an in-memory EmailRateLimiter on UsersServiceImpl with two scopes: - Per-(app, recipient): 5 calls per rolling 1h window - Per-app daily cap: 200 calls per rolling 24h window Both windows reset lazily. Token-bucket counts increment only after both checks pass — a denied recipient doesn't consume the app counter. Wired: - send_verification_email: returns UsersError::EmailRateLimited (→ 429 on the admin HTTP surface). - request_password_reset: returns Ok(()) silently on rate-limit (would otherwise enable an existence-leak side channel). UsersError::EmailRateLimited variant added; AppUsersApiError gains the same variant + 429 mapping. NoopUsersError trait stub unaffected. AUDIT.md anchor: F-S-002. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -170,6 +170,14 @@ pub enum UsersError {
|
||||
#[error("email transport: {0}")]
|
||||
EmailTransport(String),
|
||||
|
||||
/// Email-tied surface (verification / password reset / invite) was
|
||||
/// rate-limited. Either the per-(app, recipient) burst limit or the
|
||||
/// per-app daily cap was exceeded. Surfaces silently to script-land
|
||||
/// in the reset path (to avoid an existence-leak side channel) but
|
||||
/// is raised explicitly to admin-mediated callers.
|
||||
#[error("email send rate-limited")]
|
||||
EmailRateLimited,
|
||||
|
||||
/// Underlying storage error. Surfaces to script-land as a generic
|
||||
/// runtime error; the message is logged server-side for forensics.
|
||||
#[error("backend error: {0}")]
|
||||
|
||||
Reference in New Issue
Block a user