Files

72 lines
3.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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