Skip to content
This repository was archived by the owner on Aug 14, 2026. It is now read-only.
This repository was archived by the owner on Aug 14, 2026. It is now read-only.

Waitlist: timing side-channel partially defeats email-enumeration masking on mail-trigger endpoints #261

Description

@D-G-R-I-D

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.

  • 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/requestRequestWaitlistCancellation
  • POST /api/waitlist/resendResendWaitlistConfirmation
  • 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions