Raxx · internal docs

internal · gated

DET-BETA-005 — join token enumeration

Rule ID: DET-BETA-005 Title: High volume of invalid-token state-check requests from one IP — beta join endpoint probe Category: beta Last validated: 2026-06-18 (beta-phase2 catalog; grounded against as-built beta_join.py) State: liveFLAG_BETA_PHASE2_ACCESS is ON in prod; GET /api/beta/join/<token>/state is internet-reachable (CF-Access-bypassed for NDA-gated invite flow). Rate-limit events log to Raptor stdout today. Monitoring starts immediately.

Why this detection exists

GET /api/beta/join/<token>/state is the token-validity probe endpoint. It is intentionally anti-enumerating: the route returns HTTP 200 for all token states (valid, invalid, expired, revoked) — the response body {"valid": false} is the only distinguishing signal. This design prevents an attacker from using HTTP status codes to confirm token existence, but the response-body signal is machine-readable. An actor querying this endpoint at volume will observe {"valid": false} responses and iterate tokens, even though guessing an HMAC-SHA256 token is computationally infeasible. The observable is the volume, not the success rate.

The endpoint is rate-limited at 10 requests/minute per IP (@rate_limit(requests_per_window=10, window_seconds=60) in backend_v2/api/routes/beta_join.py). A sustained campaign is detectable as a series of rate-limit events across the rolling window rather than individual large bursts.

Per the design in docs/architecture/beta-phase2-join-flow.md §12, HMAC tokens with sufficient entropy make brute force impractical, but a volume spike warrants investigation for: (a) HMAC key exposure followed by targeted token generation, or (b) token harvesting from email forwarding and then systematic state-probing to find unconsumed tokens.

Telemetry source

Telemetry gap — CRITICAL: Heroku Logplex drain to a queryable store is not yet wired (prerequisite P1 from the June 4 detection catalog). Until the drain exists, this detection runs as a manual heroku logs grep rather than an automated query. Rule is marked live but manual until the drain is wired.

Statistical method + baseline window

Note: distinguishing {"valid": false} from {"valid": true} requires response-body inspection, available in Raptor logs only if the token-verify outcome is logged. Until the drain is wired, total request count per IP to the state endpoint is the practical proxy — {"valid": true} responses are rare (one per tester per token), so sustained volume is almost certainly probe traffic.

Threshold + expected FP rate

Alert route

Escalation owner

VPN operator cross-check

Per user_uses_vpn: Datacamp/CDN77 ASN ranges are operator VPN exits. Before filing any alert, verify source IP is not in those ranges.

Test fixture / synthetic positive

See _fixtures/join_token_enumeration_positive.json — 14 synthetic state-check requests from 192.0.2.20 (/24 prefix: 192.0.2.0/24) within 4 minutes, all returning {"valid": false}, token slugs are random HMAC-format values showing no sequential structure.

Manual query (until Heroku drain exists)

heroku logs --app raxx-api-prod --num 2000 \
  | grep 'beta/join' \
  | grep '/state'

Group by the source IP in the X-Forwarded-For field (leftmost IP, set by Cloudflare edge). Count per IP per 5-minute bucket. Any IP with > 10 entries in a bucket triggers the rule.

What NOT to do