Add CSP header to all HTTP responses for defense-in-depth against XSS.
The policy restricts all resource loading to same-origin and disables dangerous features (object embeds, framing, base tag injection). The embedded SPA requires no inline scripts or inline style attributes (Preact applies styles programmatically via DOM properties), so a strict policy without unsafe-inline works correctly.
Add CSP header to all HTTP responses for defense-in-depth against XSS.
The policy restricts all resource loading to same-origin and disables dangerous features (object embeds, framing, base tag injection). The embedded SPA requires no inline scripts or inline style attributes (Preact applies styles programmatically via DOM properties), so a strict policy without `unsafe-inline` works correctly.
**Directives:**
- `default-src 'self'` — baseline same-origin restriction
- `script-src 'self'` — same-origin scripts only
- `style-src 'self'` — same-origin stylesheets only
- `connect-src 'self'` — same-origin fetch/XHR only
- `img-src 'self'` — same-origin images only
- `font-src 'self'` — same-origin fonts only
- `object-src 'none'` — no plugin content
- `frame-ancestors 'none'` — prevent clickjacking
- `base-uri 'self'` — prevent base tag injection
- `form-action 'self'` — restrict form submissions
closes https://git.eeqj.de/sneak/chat/issues/41
Add CSP header to all HTTP responses for defense-in-depth against XSS.
The policy restricts all resource loading to same-origin and disables
dangerous features (object embeds, framing, base tag injection). The
embedded SPA requires no inline scripts or inline style attributes
(Preact applies styles programmatically via DOM properties), so a
strict policy without 'unsafe-inline' works correctly.
Directives:
default-src 'self' — baseline same-origin restriction
script-src 'self' — same-origin scripts only
style-src 'self' — same-origin stylesheets only
connect-src 'self' — same-origin fetch/XHR only
img-src 'self' — same-origin images only
font-src 'self' — same-origin fonts only
object-src 'none' — no plugin content
frame-ancestors 'none' — prevent clickjacking
base-uri 'self' — prevent base tag injection
form-action 'self' — restrict form submissions
CSP middleware exists — CSP() method on *Middleware in internal/middleware/middleware.go sets Content-Security-Policy header via writer.Header().Set()
Policy is strict — no 'unsafe-inline' or 'unsafe-eval' anywhere in the policy
connect-src 'self' — covers same-origin fetch/XHR used by the SPA's long-poll loop and API calls
SPA compatibility verified — web/src/index.html has no inline scripts or <style> tags; external stylesheet via <link> and external script via <script type="module">. JSX style={{...}} props in app.jsx (e.g. style={{ color: nickColor(...) }}) use Preact's CSSOM-based DOM manipulation (element.style[prop] = value), which is not restricted by CSP style-src — only HTML-parsed style="" attributes and <style> elements are blocked
Middleware wired correctly — srv.router.Use(srv.mw.CSP()) in routes.go, placed after CORS and before Timeout in the middleware chain
No linter/CI/test modifications — diff touches only middleware.go, routes.go, and README.md
README updated — Transport Security section documents the CSP header
The policy goes beyond the issue's suggested minimum (default-src 'self'; script-src 'self'; style-src 'self') by adding explicit directives for connect, img, font, object, frame-ancestors, base-uri, and form-action — all appropriate hardening.
No missing directives of concern — worker-src, child-src, manifest-src, media-src all fall back to default-src 'self' which is correct for this SPA.
Notes
CSP header is applied to all responses (including API JSON endpoints). This is harmless — browsers ignore CSP on non-document responses — and is a common practice.
The const cspPolicy string concatenation approach keeps the policy readable and maintainable.
## Code Review: PR #64 — CSP Middleware
**Verdict: ✅ PASS**
### Checklist
- [x] **CSP middleware exists** — `CSP()` method on `*Middleware` in `internal/middleware/middleware.go` sets `Content-Security-Policy` header via `writer.Header().Set()`
- [x] **Policy is strict** — no `'unsafe-inline'` or `'unsafe-eval'` anywhere in the policy
- [x] **`connect-src 'self'`** — covers same-origin fetch/XHR used by the SPA's long-poll loop and API calls
- [x] **SPA compatibility verified** — `web/src/index.html` has no inline scripts or `<style>` tags; external stylesheet via `<link>` and external script via `<script type="module">`. JSX `style={{...}}` props in `app.jsx` (e.g. `style={{ color: nickColor(...) }}`) use Preact's CSSOM-based DOM manipulation (`element.style[prop] = value`), which is not restricted by CSP `style-src` — only HTML-parsed `style=""` attributes and `<style>` elements are blocked
- [x] **Middleware wired correctly** — `srv.router.Use(srv.mw.CSP())` in `routes.go`, placed after CORS and before Timeout in the middleware chain
- [x] **No linter/CI/test modifications** — diff touches only `middleware.go`, `routes.go`, and `README.md`
- [x] **README updated** — Transport Security section documents the CSP header
- [x] **Docker build passes** — verified locally, all stages (web-builder, lint, fmt-check, test, build) succeed
### Security Review
The CSP policy is appropriately restrictive:
| Directive | Value | Assessment |
|-----------|-------|------------|
| `default-src` | `'self'` | Good baseline fallback |
| `script-src` | `'self'` | No unsafe-inline/eval ✅ |
| `style-src` | `'self'` | No unsafe-inline ✅ |
| `connect-src` | `'self'` | Covers API + long-poll ✅ |
| `img-src` | `'self'` | Same-origin images ✅ |
| `font-src` | `'self'` | Same-origin fonts ✅ |
| `object-src` | `'none'` | Blocks plugin content ✅ |
| `frame-ancestors` | `'none'` | Clickjacking protection ✅ |
| `base-uri` | `'self'` | Prevents base tag injection ✅ |
| `form-action` | `'self'` | Restricts form targets ✅ |
The policy goes beyond the issue's suggested minimum (`default-src 'self'; script-src 'self'; style-src 'self'`) by adding explicit directives for connect, img, font, object, frame-ancestors, base-uri, and form-action — all appropriate hardening.
No missing directives of concern — `worker-src`, `child-src`, `manifest-src`, `media-src` all fall back to `default-src 'self'` which is correct for this SPA.
### Notes
- CSP header is applied to all responses (including API JSON endpoints). This is harmless — browsers ignore CSP on non-document responses — and is a common practice.
- The `const cspPolicy` string concatenation approach keeps the policy readable and maintainable.
Clean, minimal, correct implementation. Fully satisfies [issue #41](https://git.eeqj.de/sneak/chat/issues/41).
<!-- session: agent:sdlc-manager:subagent:be5202f4-ab10-4fba-aff7-fa4961b5d7c5 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Add CSP header to all HTTP responses for defense-in-depth against XSS.
The policy restricts all resource loading to same-origin and disables dangerous features (object embeds, framing, base tag injection). The embedded SPA requires no inline scripts or inline style attributes (Preact applies styles programmatically via DOM properties), so a strict policy without
unsafe-inlineworks correctly.Directives:
default-src 'self'— baseline same-origin restrictionscript-src 'self'— same-origin scripts onlystyle-src 'self'— same-origin stylesheets onlyconnect-src 'self'— same-origin fetch/XHR onlyimg-src 'self'— same-origin images onlyfont-src 'self'— same-origin fonts onlyobject-src 'none'— no plugin contentframe-ancestors 'none'— prevent clickjackingbase-uri 'self'— prevent base tag injectionform-action 'self'— restrict form submissionscloses sneak/chat#41
Code Review: PR #64 — CSP Middleware
Verdict: ✅ PASS
Checklist
CSP()method on*Middlewareininternal/middleware/middleware.gosetsContent-Security-Policyheader viawriter.Header().Set()'unsafe-inline'or'unsafe-eval'anywhere in the policyconnect-src 'self'— covers same-origin fetch/XHR used by the SPA's long-poll loop and API callsweb/src/index.htmlhas no inline scripts or<style>tags; external stylesheet via<link>and external script via<script type="module">. JSXstyle={{...}}props inapp.jsx(e.g.style={{ color: nickColor(...) }}) use Preact's CSSOM-based DOM manipulation (element.style[prop] = value), which is not restricted by CSPstyle-src— only HTML-parsedstyle=""attributes and<style>elements are blockedsrv.router.Use(srv.mw.CSP())inroutes.go, placed after CORS and before Timeout in the middleware chainmiddleware.go,routes.go, andREADME.mdSecurity Review
The CSP policy is appropriately restrictive:
default-src'self'script-src'self'style-src'self'connect-src'self'img-src'self'font-src'self'object-src'none'frame-ancestors'none'base-uri'self'form-action'self'The policy goes beyond the issue's suggested minimum (
default-src 'self'; script-src 'self'; style-src 'self') by adding explicit directives for connect, img, font, object, frame-ancestors, base-uri, and form-action — all appropriate hardening.No missing directives of concern —
worker-src,child-src,manifest-src,media-srcall fall back todefault-src 'self'which is correct for this SPA.Notes
const cspPolicystring concatenation approach keeps the policy readable and maintainable.Clean, minimal, correct implementation. Fully satisfies issue #41.