feat(security): add security response headers [operator-action] (LC-504) #471

Merged
Claude-Run merged 1 commit from feat/LC-504-security-headers into main 2026-06-28 03:15:26 +02:00
Member

lets-chat previously returned no transport/framing/sniffing/policy headers on any response; only the session cookie was hardened. This adds a set_security_headers tower middleware applied as the OUTERMOST layer of build_router, so every response shape (HTML pages, HTMX fragments, the JSON API, webhooks, feeds, static assets, redirects and error responses) carries all six headers.

The values are tuned to the existing HTMX/Askama app rather than a from-scratch lockdown, so they cannot break shipped functionality: the CSP keeps 'unsafe-inline'/'unsafe-eval' for the inline <script> blocks and htmx hx-on::* handlers, allows data:/blob: plus the Tenor CDN in img-src for avatar/upload previews and the LC-488 GIF picker grid, and allows ws:/wss: in connect-src for the live hub; the real hardening wins are frame-ancestors 'none', object-src 'none', base-uri 'self' and form-action 'self'. Permissions-Policy keeps camera/microphone/display-capture for the WebRTC call surface and denies geolocation and the rest. The middleware only inserts a header the handler did not already set, so a future route can override.

A new server/tests/security_headers.rs asserts all six headers are present on a public 200, an authed page and a redirect, and pins the X-Frame-Options/nosniff/CSP/Permissions-Policy values.

Operator-Action: lets-chat now emits HSTS (max-age=31536000; includeSubDomains; preload), Content-Security-Policy, X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy and Permissions-Policy on every response; if you terminate TLS at a fronting proxy that also injects these headers reconcile them to avoid duplicates, and be aware the HSTS includeSubDomains/preload directive now applies to the whole domain for a year.

#LC-504

lets-chat previously returned no transport/framing/sniffing/policy headers on any response; only the session cookie was hardened. This adds a `set_security_headers` tower middleware applied as the OUTERMOST layer of `build_router`, so every response shape (HTML pages, HTMX fragments, the JSON API, webhooks, feeds, static assets, redirects and error responses) carries all six headers. The values are tuned to the existing HTMX/Askama app rather than a from-scratch lockdown, so they cannot break shipped functionality: the CSP keeps `'unsafe-inline'`/`'unsafe-eval'` for the inline `<script>` blocks and htmx `hx-on::*` handlers, allows `data:`/`blob:` plus the Tenor CDN in `img-src` for avatar/upload previews and the LC-488 GIF picker grid, and allows `ws:`/`wss:` in `connect-src` for the live hub; the real hardening wins are `frame-ancestors 'none'`, `object-src 'none'`, `base-uri 'self'` and `form-action 'self'`. Permissions-Policy keeps camera/microphone/display-capture for the WebRTC call surface and denies geolocation and the rest. The middleware only inserts a header the handler did not already set, so a future route can override. A new `server/tests/security_headers.rs` asserts all six headers are present on a public 200, an authed page and a redirect, and pins the X-Frame-Options/nosniff/CSP/Permissions-Policy values. Operator-Action: lets-chat now emits HSTS (max-age=31536000; includeSubDomains; preload), Content-Security-Policy, X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy and Permissions-Policy on every response; if you terminate TLS at a fronting proxy that also injects these headers reconcile them to avoid duplicates, and be aware the HSTS includeSubDomains/preload directive now applies to the whole domain for a year. #LC-504
feat(security): add security response headers [operator-action] (LC-504)
All checks were successful
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 7s
check-secrets / TruffleHog (push) Successful in 9s
check-secrets / TruffleHog (pull_request) Successful in 10s
check-secrets / Kingfisher (pull_request) Successful in 11s
check-secrets / Kingfisher (push) Successful in 12s
Check / clippy + fmt + tests (pull_request) Successful in 5m29s
Create release / Create release from merged PR (pull_request) Has been skipped
cdb5b411ed
lets-chat previously returned no transport/framing/sniffing/policy headers on any response; only the session cookie was hardened. This adds a `set_security_headers` tower middleware applied as the OUTERMOST layer of `build_router`, so every response shape (HTML pages, HTMX fragments, the JSON API, webhooks, feeds, static assets, redirects and error responses) carries all six headers.

The values are tuned to the existing HTMX/Askama app rather than a from-scratch lockdown, so they cannot break shipped functionality: the CSP keeps `'unsafe-inline'`/`'unsafe-eval'` for the inline `<script>` blocks and htmx `hx-on::*` handlers, allows `data:`/`blob:` plus the Tenor CDN in `img-src` for avatar/upload previews and the LC-488 GIF picker grid, and allows `ws:`/`wss:` in `connect-src` for the live hub; the real hardening wins are `frame-ancestors 'none'`, `object-src 'none'`, `base-uri 'self'` and `form-action 'self'`. Permissions-Policy keeps camera/microphone/display-capture for the WebRTC call surface and denies geolocation and the rest. The middleware only inserts a header the handler did not already set, so a future route can override.

A new `server/tests/security_headers.rs` asserts all six headers are present on a public 200, an authed page and a redirect, and pins the X-Frame-Options/nosniff/CSP/Permissions-Policy values.

Operator-Action: lets-chat now emits HSTS (max-age=31536000; includeSubDomains; preload), Content-Security-Policy, X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy and Permissions-Policy on every response; if you terminate TLS at a fronting proxy that also injects these headers reconcile them to avoid duplicates, and be aware the HSTS includeSubDomains/preload directive now applies to the whole domain for a year.

#LC-504
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!471
No description provided.