You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The waitlist endpoints that email an arbitrary address on request return the same masked response body whether or not the email is on the waitlist — but the response timing differs, which partially re-enables email enumeration.
Not-on-waitlist / ineligible → returns after just a FindByEmail lookup (fast).
On-waitlist & eligible → also does a Redis cooldown claim, token generation, (for resend) a DB write, and an awaited SMTP send before returning (slower).
An attacker measuring latency can infer membership despite the masked body.
Affected endpoints
POST /api/waitlist/cancel/request — RequestWaitlistCancellation
POST /api/waitlist/resend — ResendWaitlistConfirmation
POST /api/waitlist/join notice paths — JoinWaitlist (already-on-waitlist / already-registered)
Current mitigations (why this is low severity)
All endpoints are rate-limited (GeneralPolicy), capping how many timing samples an attacker can gather.
The dominant cost is the SMTP round-trip, whose latency is inherently noisy — a weak, unreliable signal.
Response bodies are already masked (generic success), so there's no direct oracle.
Proper fix (recommended)
Move the entry-specific work (eligibility lookup + throttle + send) off the request path into a dedicated waitlist-mail background worker, so every request returns immediately with uniform timing. The codebase already has the building blocks (Redis producer/consumer for scans; BackgroundService workers under src/Web/Workers), but no generic enqueue abstraction — this needs:
a mail-job payload + Redis producer (enqueue "send waitlist mail X to address Y"),
a new BackgroundService consumer that dequeues, re-checks eligibility/throttle, and sends,
migrating the eligibility logic out of the handlers, with tests.
This also uniformly benefits resend/cancellation/notice flows and would let the throttle live in one place.
Alternatives considered (and why not)
Fire-and-forget the send (don't await SMTP): only partially closes the gap (throttle/DB work still diverges) and risks unobserved exceptions + disposed request scopes.
Artificial equalizing delays: add latency to every request and are fragile to tune against real send times.
Not done inline because
It's a real feature (new async mail pipeline), a different concern from the waitlist feature PR, and low enough severity that a half-measure would trade a theoretical leak for concrete reliability/latency costs.
Context: follow-up from PR #177 (CodeRabbit flagged the resend path as a 🟠 Major security item; same shape applies to the sibling endpoints).
Summary
The waitlist endpoints that email an arbitrary address on request return the same masked response body whether or not the email is on the waitlist — but the response timing differs, which partially re-enables email enumeration.
FindByEmaillookup (fast).An attacker measuring latency can infer membership despite the masked body.
Affected endpoints
POST /api/waitlist/cancel/request—RequestWaitlistCancellationPOST /api/waitlist/resend—ResendWaitlistConfirmationPOST /api/waitlist/joinnotice paths —JoinWaitlist(already-on-waitlist / already-registered)Current mitigations (why this is low severity)
GeneralPolicy), capping how many timing samples an attacker can gather.Proper fix (recommended)
Move the entry-specific work (eligibility lookup + throttle + send) off the request path into a dedicated waitlist-mail background worker, so every request returns immediately with uniform timing. The codebase already has the building blocks (Redis producer/consumer for scans;
BackgroundServiceworkers undersrc/Web/Workers), but no generic enqueue abstraction — this needs:BackgroundServiceconsumer that dequeues, re-checks eligibility/throttle, and sends,This also uniformly benefits resend/cancellation/notice flows and would let the throttle live in one place.
Alternatives considered (and why not)
Not done inline because
It's a real feature (new async mail pipeline), a different concern from the waitlist feature PR, and low enough severity that a half-measure would trade a theoretical leak for concrete reliability/latency costs.
Context: follow-up from PR #177 (CodeRabbit flagged the resend path as a 🟠 Major security item; same shape applies to the sibling endpoints).