feat: add Content-Security-Policy middleware #64

Merged
sneak merged 2 commits from feat/csp-headers into main 2026-03-10 11:20:15 +01:00
Collaborator

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 #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 closes https://git.eeqj.de/sneak/chat/issues/41
clawbot added 1 commit 2026-03-10 11:15:15 +01:00
feat: add Content-Security-Policy middleware
All checks were successful
check / check (push) Successful in 4s
369eef7bc3
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
clawbot added the botneeds-review labels 2026-03-10 11:15:21 +01:00
clawbot added 1 commit 2026-03-10 11:15:56 +01:00
docs: document CSP header in Security Model section
All checks were successful
check / check (push) Successful in 1m4s
d6cfb2e897
clawbot removed the needs-review label 2026-03-10 11:16:24 +01:00
clawbot added the needs-review label 2026-03-10 11:17:27 +01:00
Author
Collaborator

Code Review: PR #64 — CSP Middleware

Verdict: PASS

Checklist

  • CSP middleware existsCSP() 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 verifiedweb/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 correctlysrv.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
  • 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.

## 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 -->
clawbot added merge-ready and removed needs-reviewbot labels 2026-03-10 11:19:20 +01:00
sneak was assigned by clawbot 2026-03-10 11:19:31 +01:00
sneak merged commit a98e0ca349 into main 2026-03-10 11:20:15 +01:00
sneak deleted branch feat/csp-headers 2026-03-10 11:20:15 +01:00
Sign in to join this conversation.