Skip to content

Threat Model

What we're defending against, what we do about it, and where in the code the defense lives. This is the high-level catalogue — operational tuning of each control lives in the linked sub-pages.

Credential stuffing and brute force

Threat. Attackers replay credential lists against /auth/login, /auth/webauthn/login/begin, /auth/otp/verify, etc., or hammer registration to enumerate emails.

Mitigation.

  • Per-IP rate limits — 10 logins / 15 min, 5 registrations / hour, 5 password resets / hour.
  • Bcrypt password hashing with a default cost factor — slow by design, takes years to brute force a single hash.
  • Failed logins logged in the audit trail (AuditService) so anomalies surface in analytics.
  • Auto-IP-block after 5 rate-limit violations within an hour (24-hour block).

Where. backend/internal/middleware/rate_limiter.go, backend/internal/middleware/ip_blocker.go. See Security → Rate Limiting.

Account farming / sybil attacks

Threat. Mass account creation to game social proof, spam events, or harvest invitations.

Mitigation. Layered, independent gates:

  1. IP rate limit (5 registrations / hour).
  2. Email OTP at signup — must own the address.
  3. Device fingerprint cap — 3 accounts per device per 30 days.
  4. Optional phone verification for event creation (extra friction).

Where. backend/internal/services/auth_service.go (checkDeviceFingerprint). See Security → Device Fingerprint.

SQL injection

Threat. Malicious input crafted to alter or read SQL beyond the intended query.

Mitigation.

  • GORM-only query construction. Repositories use the query builder, which always parameterises values. Raw SQL is restricted to a small set of PostGIS queries that use ? placeholders — verifiable by grep "db.Raw\|gorm.io" across backend/internal/repository/.
  • Heuristic block at the edge — IPBlocker.SuspiciousActivityMiddleware detects classic injection signatures (' OR '1'='1, UNION SELECT, etc.) in query params and 403s the request with a 48-hour IP block.

Where. backend/internal/middleware/ip_blocker.go, every file in backend/internal/repository/.

SSRF (server-side request forgery)

Threat. The /api/v1/link-preview endpoint fetches arbitrary URLs to extract OG metadata. Without protection, it could reach internal services (metadata endpoints, internal HTTP APIs).

Mitigation.

  • Scheme allowlist — https only.
  • Hostname resolution + private-IP rejection — net.LookupHost, then for each resolved IP, reject if it falls in any RFC1918 / loopback / link-local / multicast range.
// backend/internal/handlers/link_preview_handler.go
if u.Scheme != "https" { return fmt.Errorf("only https URLs are allowed") }
hostname := u.Hostname()
addrs, _ := net.LookupHost(hostname)
for _, addr := range addrs {
    ip := net.ParseIP(addr)
    if ip == nil || isPrivateIP(ip) {
        return fmt.Errorf("target resolves to a private IP address")
    }
}

Where. backend/internal/handlers/link_preview_handler.go::validateURL.

CSRF

Threat. A malicious site causes an authenticated user's browser to make state-changing requests.

Mitigation. Largely not applicable for our API:

  • Auth is bearer-token, not cookie. Browsers don't auto-attach Authorization headers across origins.
  • CORS allows * in dev (locked down in prod), but credentials in headers must be explicitly set by the calling code — there's no implicit auth.
  • The only cookie-style risks would be on the webhook endpoints, which use their own signing/verification (Stripe webhook signature, Apple notification JWT signature).

We do not issue or accept CSRF tokens.

XSS

Threat. User-supplied content rendered into HTML executes attacker JavaScript.

Mitigation.

  • API returns JSON, never HTML. The only HTML the backend emits is the public event share page (/api/v1/share/events/:id), which renders through Go's html/template (auto-escaping) and only includes fields validated server-side.
  • X-Content-Type-Options: nosniff and X-Frame-Options: DENY on every response via SecurityHeaders middleware.
  • HSTS (Strict-Transport-Security: max-age=31536000; includeSubDomains) on production responses.
  • Frontend uses React (auto-escapes JSX) and adds a CSP at the document level — see frontend docs.

Where. backend/internal/middleware/security.go, backend/templates/og.html.

Bot / spam

Threat. Automated event spam, fake messages, drive-by signups.

Mitigation.

  • Email OTP required for signup, password reset, and email change — eliminates throwaway-without-email-access flows.
  • Rate limits on the public auth surface (above).
  • Suspicious-activity middleware catches obvious automated scanning.
  • Audit log of all activity (AuditService) gives ops a paper trail for after-the-fact takedowns.

Data exposure at rest

Threat. A DB dump exposes passwords, API keys, or PII directly.

Mitigation.

  • Passwords: bcrypt-hashed, never stored plaintext.
  • API keys (for the /api/v1/auth/api-keys feature): the plaintext is shown to the user once at create time; what we store is the SHA-256 hash for lookup plus an AES-GCM-encrypted copy keyed by ENCRYPTION_KEY (in Security.EncryptionKey). Compromising the DB alone is not enough — the encryption key is in GCP Secret Manager, separate.
  • OAuth tokens (Google, Apple, LINE access tokens, when persisted) are encrypted at rest using the same key.
  • JWTs are never stored — they're verified statelessly and forgotten.
  • Refresh tokens are stored hashed in RefreshToken and looked up by hash.
  • PII minimisation — we keep email, optional phone, optional display name, and an avatar URL. No address, no DOB, no payment card data (Stripe holds card details).

Where. backend/internal/services/auth_service.go, backend/internal/repository/api_key_repository.go, backend/config/config.go (SecurityConfig.EncryptionKey).

Privilege escalation

Threat. A non-admin user calls an admin endpoint.

Mitigation. Two-layer auth:

  1. JWTAuth resolves the user from the bearer token.
  2. AdminAuth (mounted on /admin/*) re-reads the user from the DB and checks IsAdmin == true. Re-reading rather than trusting the JWT claim means privileges can be revoked instantly.

Where. backend/internal/middleware/admin.go.

Transport security

Threat. Plain-HTTP requests, downgrade attacks, missing certificate pinning.

Mitigation. TLS is terminated by the load balancer (Cloud Run / GKE Ingress). HSTS is enabled in production. Mobile clients pin to LetsEncrypt issuers transitively (no static cert pinning today).

Out of scope / known gaps

Gap Impact Mitigation status
WebSocket multi-instance fan-out Chat splits across replicas Single-replica deploy as workaround
Per-user rate limits on authenticated routes Compromised account can spam Rely on IP + audit log
Captcha on registration Determined attacker can defeat the email OTP loop Considered for next milestone
Production WAF No L7 inspection beyond our middleware LB-level WAF on the roadmap

Use the audit log + alerting rules in operations to spot abuse that slips through these gates. For incident response, see Operations → Runbook.