Address the seven findings from the security & deployment review. High: - upload: make infer authoritative — reject files it can't identify (SVG/HTML/JS) and require the detected MIME to be on an ALLOWED_MEDIA allowlist; derive the stored MIME and on-disk extension from the detected type, ignoring client filename/Content-Type. Closes the stored-XSS vector via media served on-origin. - deploy: rename docker-compose.override.yml -> docker-compose.dev.yml so the default `docker compose up -d` no longer publishes Postgres 5432 to the host; the port map is now opt-in via -f. README updated. Medium: - upload: DefaultBodyLimit::disable() -> max(576 MiB) as an HTTP-level OOM backstop; handler still enforces precise per-class size limits. - docker: run backend and frontend as non-root users. Low: - social/upload: event-scope toggle_like, list_comments, add_comment, delete_comment, edit_upload, delete_upload via find_by_id_and_event / soft_delete_in_event — cross-event IDs now resolve to 404. - Caddy: site-wide HSTS / nosniff / X-Frame-Options / Referrer-Policy, plus Content-Disposition: attachment on /media/originals/*. - .env.example: replace default Postgres password with a CHANGE_ME hint. Out of scope: localStorage JWT (root cause fixed; httpOnly cookies are a larger change tracked separately). Verified: cargo build (no new warnings), cargo test (3 passed), caddy validate, docker compose config (no 5432 published by default). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
39 lines
1.4 KiB
Caddyfile
39 lines
1.4 KiB
Caddyfile
{$DOMAIN} {
|
|
encode zstd gzip
|
|
|
|
# Site-wide security headers (defense-in-depth). HSTS is free since Caddy
|
|
# already terminates TLS. nosniff also covers all of /media/*.
|
|
header {
|
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
X-Content-Type-Options "nosniff"
|
|
X-Frame-Options "DENY"
|
|
Referrer-Policy "strict-origin-when-cross-origin"
|
|
}
|
|
|
|
# SvelteKit frontend — static assets with long-lived cache (content-hashed filenames)
|
|
@hashed_assets path_regexp hashed /_app/immutable/.*\.[a-f0-9]{8,}\.(js|css|woff2)$
|
|
header @hashed_assets Cache-Control "public, max-age=31536000, immutable"
|
|
|
|
# Media previews and thumbnails
|
|
@previews path /media/previews/* /media/thumbnails/*
|
|
header @previews Cache-Control "public, max-age=3600"
|
|
|
|
# Original media files (private — only host can download). Force download
|
|
# rather than inline rendering as defense-in-depth against any future
|
|
# content-type confusion (previews/thumbnails are re-encoded and stay inline).
|
|
@originals path /media/originals/*
|
|
header @originals Cache-Control "private, max-age=86400"
|
|
header @originals Content-Disposition "attachment"
|
|
|
|
# API — never cache
|
|
@api path /api/*
|
|
header @api Cache-Control "no-store"
|
|
|
|
# Route API and media requests to the Rust backend
|
|
reverse_proxy /api/* app:3000
|
|
reverse_proxy /media/* app:3000
|
|
|
|
# Everything else goes to SvelteKit frontend
|
|
reverse_proxy frontend:3001
|
|
}
|