First commit, phased 1 and 2 created but untested
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user