Files
Emberfall/docs/SECURITY.md
T

3.6 KiB
Raw Blame History

SECURITY.md

PBBGs are cheat targets. Assume every request is hostile.

Authentication

  • Hash with bcryptjs, cost 10–12. Never store or log plaintext.
  • Case-insensitive unique usernames (DB index uses COLLATE NOCASE).
  • Login errors are generic: INVALID_CREDENTIALS. Do a dummy hash compare when the user doesn't exist (timing).
  • Regenerate the session ID on login (req.session.regenerate) to prevent fixation.
  • Logout destroys the session server-side and clears the cookie.
  • Banned users (is_banned=1) are rejected at login and in requireAuth.

Sessions / cookies

  • httpOnly: true, sameSite: 'lax', secure: true when NODE_ENV=production.
  • maxAge 7 days, rolling. Secret from SESSION_SECRET env (refuse to boot in production without it).
  • Store sessions in SQLite, not memory (so restarts don't log everyone out).

CSRF

Cookie-based sessions + state-changing POSTs ⇒ CSRF matters.

  • sameSite=lax blocks most cross-site POSTs.
  • Additionally require header Content-Type: application/json on all POSTs and reject others (browsers can't send cross-site JSON without CORS preflight). Do not enable CORS.

Input validation

  • Validate every body/query/param with zod. Reject unknown keys (.strict()).
  • Clamp numerics (quantities 1–99). Reject NaN/negative/float where ints are expected.
  • Never render user-supplied text with innerHTML. Use textContent. (Usernames and hero names are user-supplied!)

Anti-cheat / game integrity

  • Server decides all outcomes; client sends only intent.
  • Every multi-step state change is one DB transaction (spend + grant).
  • Ownership: derive the character from the session — never accept characterId from the client.
  • Check inventory.character_id = ? on every inventory operation.
  • Spam: rate-limit actions (e.g., 30 game actions / minute / user) — energy is the real limit, this is a safety net.
  • Expedition claim: set claimed_at and grant rewards in one transaction; the partial unique index prevents duplicates.
  • Never trust client clocks. All timing from utils/time.now().

Rate limiting (express-rate-limit)

Scope Limit
POST /auth/login 10 / 15 min / IP
POST /auth/register 5 / hour / IP
other /api 120 / min / IP

Headers & transport

  • helmet() defaults. Provide a CSP: default-src 'self' (no inline scripts — put JS in files).
  • Serve behind HTTPS in production (note in README; reverse proxy is out of scope).
  • app.set('trust proxy', 1) only if running behind a proxy.

Data exposure

  • Public endpoints (/leaderboard, /heroes/:name) return only: name, class, level, kills, gold(optional). Never user ids, usernames, or emails.
  • Hero name ≠ login username (so leaderboard doesn't reveal login names).
  • Error handler returns generic message for unexpected errors; stack traces go to the server log only.

SQL

  • Prepared statements with ? / named params only. No string-built SQL, ever. Table/column names are never user-controlled (use allowlist for leaderboard sort type).

Secrets & logging

  • .env is gitignored. .env.example has placeholders only.
  • Don't log request bodies on auth routes.
  • audit_log records login ok/fail, register, logout with IP + timestamp.

Checklist (Phase 9.1) — tick each in STATUS Session Log

  • No innerHTML with dynamic data
  • All routes validated by zod
  • All writes in transactions
  • Ownership checks on every inventory/expedition op
  • Rate limiters mounted
  • Cookie flags verified in browser dev tools
  • Password hash never in any response (grep for password_hash in routes)
  • Leaderboard sort allowlisted
  • Production boot fails without SESSION_SECRET