fix(security): critical — repair PIN reset + enforce real prod secrets
C1: reset_user_pin wrote to a non-existent column (pin_failed_attempts);
the real column is failed_pin_attempts, so every PIN reset 500'd. Fixed
the column name; new e2e (pin-reset.spec.ts) proves a reset returns a
usable PIN and the target can recover with it.
C2: config.rs::validate_secrets now rejects placeholder-ish secrets
(change_me/dev_secret/placeholder), enforces len>=32 in prod, and
requires a real ADMIN_PASSWORD_HASH. docker-compose.yml sets
APP_ENV=production so the guard actually runs. Corrected the false
"fixed" claim in SECURITY-BACKLOG.md. .env.example documents the rule.
Riders in these files (documented here since git can't split hunks):
- host.rs also carries the event-scoped ban_user fix and the
close_event/open_event no-op broadcast guard (medium).
- docker-compose.yml also adds ORIGIN (H6), app/frontend healthchecks with
Caddy waiting on health, and per-service memory limits (medium).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -51,15 +51,25 @@ item below is tagged with its **current status in `main`**:
|
||||
|
||||
## ✅ Fixed in main since the audit (for the record)
|
||||
|
||||
These audit findings are present in `main` today (verified 2026-06-30): event-scoped social
|
||||
handlers (cross-event authz), server-side MIME/ext allowlist on upload, recovery-PIN lockout
|
||||
backoff, unspoofable client IP in the rate limiter, effective JWT production-secret guard, DB port
|
||||
no longer publicly exposed, container healthchecks, bcrypt offloaded via `spawn_blocking`,
|
||||
bounded compression concurrency (semaphore), bounded feed queries (`LIMIT ≤ 100`), and the
|
||||
viewport-fit / reduced-motion / aria a11y pass. **New since the audit:** an image-decode
|
||||
decompression-bomb cap (`image::Limits` 12000×12000 / 256 MiB) ported into
|
||||
These audit findings are present in `main` today (verified 2026-06-30): server-side MIME/ext
|
||||
allowlist on upload, recovery-PIN lockout backoff, DB port no longer publicly exposed, bcrypt
|
||||
offloaded via `spawn_blocking`, bounded compression concurrency (semaphore), bounded feed queries
|
||||
(`LIMIT ≤ 100`), and the viewport-fit / reduced-motion / aria a11y pass. An image-decode
|
||||
decompression-bomb cap (`image::Limits` 12000×12000 / 256 MiB) lives in
|
||||
`backend/src/services/compression.rs`.
|
||||
|
||||
**Fixed in the 2026-07 review pass** (previously *claimed* fixed here but were not — corrected):
|
||||
- **Effective JWT production-secret guard** — `APP_ENV=production` is now set for the app service,
|
||||
and `config.rs::validate_secrets` rejects any placeholder-ish `JWT_SECRET`/`ADMIN_PASSWORD_HASH`
|
||||
(not just the exact dev sentinel) and enforces `len ≥ 32` unconditionally in prod.
|
||||
- **Unspoofable client IP in the rate limiter** — `client_ip` now takes the right-most
|
||||
`X-Forwarded-For` entry (the hop Caddy appends), so a client-supplied left-most value is ignored.
|
||||
- **Live role/ban re-check** — the auth extractor re-reads the user row and trusts the DB role and
|
||||
`is_banned` flag rather than the JWT claim, revoking demoted/banned sessions immediately.
|
||||
- **Container healthchecks** — `app` and `frontend` now have healthchecks and Caddy waits on
|
||||
`service_healthy`.
|
||||
- **Event-scoped `ban_user`** — the ban UPDATE is now scoped by `event_id` like its siblings.
|
||||
|
||||
## 🅲 Consciously won't-fix at ~100-guest single-box scale
|
||||
|
||||
Diminishing returns vs. the deployment's actual threat model. Revisit only if the scale or
|
||||
|
||||
Reference in New Issue
Block a user