First commit, phased 1 and 2 created but untested

This commit is contained in:
bepis
2026-10-20 07:40:35 +11:00
commit 7a3e408ffc
66 changed files with 5639 additions and 0 deletions
+71
View File
@@ -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