Files
EventSnap/docs/SECURITY-BACKLOG.md
MechaCat02 2d7169e971 harden: low-severity bucket — unspoofable IP, admin floor, log/SSE hygiene
The cheap, real LOWs from the review (bucket 🅰); 🅱 items and won't-fix
decisions are recorded in docs/SECURITY-BACKLOG.md.

- XFF spoofing (highest-value): client_ip now prefers an unspoofable X-Real-IP
  (Caddy `header_up X-Real-IP {remote_host}`) and otherwise takes the *rightmost*
  X-Forwarded-For token, never the client-controlled leftmost. The join cap,
  recover throttle, and admin-login floor all keyed on this. Unit-tested.
  Caddyfile also drops the dead /media/previews|originals cache matchers (the
  gateway sets its own Cache-Control) and the false "only host can download"
  comment.
- admin_login: add a hard, non-disableable rate floor (30/5min/IP) so the DB
  rate-limit master toggle can't remove brute-force protection.
- SSE: the broadcast upload-error payload no longer includes the raw error
  (absolute filesystem paths) — generic message to clients, details logged
  server-side only.
- Access logs: the request span logs the path only, never the query string, so
  the replayable media `?sig=` capability isn't written to logs.
- Reaper TOCTOU: reap_deleted now deletes rows by the ids it actually unlinked
  (DELETE … WHERE id = ANY) instead of re-evaluating the time predicate, so a
  row crossing the grace line mid-sweep can't be row-deleted without its files
  unlinked.
- Dockerfile: run the backend as a non-root user; create /media owned by `app`
  so a fresh volume is writable.
- a11y: aria-labels on the upload caption and feed search inputs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:01:41 +02:00

3.2 KiB

Security & Hardening Backlog

Tracks the deliberately-deferred items from the 2026-06-27 audit and its review passes. The Critical→Medium findings and the cheap "bucket 🅰" LOWs are fixed on fix/audit-2026-06-27-critical-medium. What remains is recorded here so the decisions are explicit, not forgotten.

🅱 Worth a tracked ticket (real, not one-liners)

  • Moderation UI gap — the backend DELETE /host/upload/{id} and DELETE /host/comment/{id} endpoints have no frontend caller, so a host cannot remove a guest's content from the UI. This is a functional hole, not polish. Needs a host-facing "remove" action wired to those endpoints (a ContextSheet action gated on host role).
  • Feed reactivity — a single global SSE event (like-update/new-comment/upload-processed) triggers loadFeed(true), which full-replaces the list with the first 20, collapsing a scrolled feed; and owner-deleted uploads aren't broadcast to other clients (only host deletes are). Patch the affected item from the SSE payload instead of full-reloading; emit upload-deleted from the owner delete_upload path too.
  • Media HMAC domain separation — the signed-media tokens reuse jwt_secret as the HMAC key. Correct today (different message structure), but a dedicated derived key (HKDF(jwt_secret, "media-url")) would isolate the domains so a future change to one can't weaken the other.
  • Quota mount-detection + low-disk guardcompute_storage_quota picks the disk via starts_with (root is a wildcard prefix → can match the wrong device) and there's no hard min-free-space precheck when the quota is disabled. Use longest-prefix match; add an unconditional 507/429 when free space is critically low.

🅲 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 tenancy model changes.

  • Rate-limiter HashMap key LRU/cap (attacker-chosen recover:{ip}:{name} keys accumulate up to the 24h prune ceiling) — bounded and pruned; not worth an LRU.
  • "Last host" / host↔host role-churn guard — operational, low blast radius.
  • Performance micro-indexes (idx_like_user_upload, comment pagination index) — current queries are sub-ms at this row count.
  • Optimistic-like in-flight guard, ownership-snapshot-at-mount, assorted copy tweaks — UX polish.

By-design notes (documented, not bugs)

  • Banned user retains ≤24h media access via already-held signed URLs. The media gateway authorizes by signature + uploader visibility, not requester identity, and signed URLs are time-boxed (24h, bucketed). A banned/revoked-session user cannot mint new URLs (every API call 401/403s) but can replay URLs they already hold until expiry — only for content they already saw. Accepted: tightening this would require per-request identity on every <img> load, which the <img>-can't-send-a-Bearer constraint precludes. Shorten the TTL in media_token.rs if a stricter bound is ever needed.
  • Two DB queries per cold media fetch (upload row + uploader row). Mitigated by browser caching and the stable bucketed URL (so warm fetches don't hit the backend at all). Could be one JOIN if it ever shows up in profiling.