Read before deploying for a real event.
- Attendee list (email addresses), valuable to spammers, phishers, recruiters.
- Contact info shared after a ride match (Signal, phone, social handle).
- Ride metadata (who travels when, from where).
Via host compromise, a public bucket, a leaked backup.
- Emails stored as
HMAC-SHA256(normalize(email), ALLOWLIST_SALT), never plaintext. - The salt is an env var, not in the DB; without it, only offline brute force against a candidate list works.
- Rotating the salt invalidates all hashes; admin re-imports the CSV.
Limit: DB plus env file (full host access) lets an attacker hash any candidate. Harden the host.
Probing /auth/send to learn who's registered.
- Same response whether or not the email is listed.
startMagicLinkadds an artificial delay on the off-list path.MAGIC_LINK_RATE_LIMIT(default 5) per email per hour; 30 per IP per hour.- No endpoint returns the allowlist.
Limit: 100 IPs at 30/hour probe ~72k addresses/day, against lists usually under 2k. Put Cloudflare or a similar WAF in front with stricter limits.
A compromised admin runs a wordlist through /admin/allowlist/check.
- 30 checks per admin per hour.
- Each check is audit-logged (timestamp, admin email, IP).
From XSS, MITM or malware.
- Opaque 32-byte tokens (
crypto.randomBytes) stored server-side; delete the row to revoke. HttpOnly,SameSite=Lax, andSecurewhenAPP_URLishttps://.- Strict CSP (no inline scripts except style; no third-party origins) and auto-escaping templates.
- 14-day default lifetime, configurable.
Query-string tokens land in history, logs and Referer headers.
- 256-bit random, single-use, 15-minute expiry.
- DB stores the token's HMAC, so log access alone yields nothing.
Referrer-Policy: same-origin.
Limit: browser-history access within 15 minutes, before first use, allows sign-in.
Forged POSTs (/rides/new, /claims/:id/accept, etc.).
SameSite=Laxsession cookie blocks cross-origin POSTs in modern browsers.Content-Security-Policy: frame-ancestors 'none'blocks framing.Permissions-Policydenies camera, microphone and other powerful features. Geolocation and screen wake lock are allowed for this origin only, for opt-in live location.
Limit: SameSite=Lax is the main defense; browsers 10+ years old that ignore it are exposed. Acceptable for a 2026 event. See docs/security/csrf.md.
No outbound requests built from user input, except the hard-coded Resend API endpoint.
The in-memory limiter resets on restart. Windows are 1 hour, so one restart doesn't enable enumeration at scale.
Limit: on a platform that restarts every deploy, under active attack, swap in a DB-backed limiter (lib/rate-limit.js).
An http:// APP_URL (local dev) drops Secure from cookies. In production use https://, and TRUST_PROXY=true behind a TLS-terminating proxy.
Small buckets identify people ("1 person flew SFO→venue at 3:14am Tuesday").
- Buckets under 5 entries merge into "Other" (k-anonymity heuristic).
- No per-user admin views. The audit log records actors for state changes only, not browsing.
- Generate
SESSION_SECRETandALLOWLIST_SALTwithopenssl rand -hex 32, fresh per event. - Encrypt disks at rest. Railway, Render and Fly volumes are encrypted by default.
- Protect backups like the live DB.
- Keep
ADMIN_EMAILSminimal. - After the event: "Wipe attendee data" in admin, or destroy the deployment and volume.
SESSION_LIFETIME_DAYS=14expires stale sessions.
- No SOC2 audit or third-party pen test. The codebase is small (~2000 lines) and readable end to end.
- No defense against a compromised admin beyond the audit log, or a compromised host (root).
- Not built for multi-tenancy or extreme scale.
Email the supportEmail in your event.config.yaml, or open an issue prefixed security: in your fork's tracker.