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>
This commit is contained in:
51
docs/SECURITY-BACKLOG.md
Normal file
51
docs/SECURITY-BACKLOG.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# 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 guard** — `compute_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.
|
||||
Reference in New Issue
Block a user