NextMyDay/internal/core/httpx/headers.go
Claus Lohmar 03a9db03dc fix(security): harden httpx — XFF hops, __Host- cookie, isolation headers
REVIEW-001 (M0) perimeter findings:

- H1: resolveForwarded walked X-Forwarded-For left→right and returned the
  leftmost untrusted entry. An append-style proxy (nginx's default) appends
  the peer it saw to the RIGHT of whatever the client sent, so the leftmost
  entry is attacker-controlled — a fresh rate-limit bucket per request (A3)
  and a forged client_ip in the audit trail (A11/D-031). Walk right→left and
  return the first untrusted entry, falling back to the peer.
- L1s: the session cookie now carries the `__Host-` prefix when it is Secure
  (https), so it is pinned to the exact origin and cannot be shadowed from a
  sibling subdomain. Conditional on purpose: a browser rejects a `__Host-`
  cookie without Secure, which would break the http loopback bootstrap.
- L4s: add Permissions-Policy, Cross-Origin-Opener-Policy and
  Cross-Origin-Resource-Policy.
- Add httpx.IsLoopbackIP: a RESOLVED client IP test that fails closed on an
  empty/unparseable value (the setup-mode gate, H2), sharing a classifier
  with IsLoopbackHost whose "unknown" direction is the opposite, each
  fail-closed for its own decision.

Tests updated: the unit test encoding the vulnerable leftmost expectation is
rewritten, plus right→left/forged-entry/host-classification coverage.
2026-10-14 11:29:52 +00:00

39 lines
1.6 KiB
Go

package httpx
import "net/http"
// ContentSecurityPolicy is the strict default CSP (SPEC-003 §4). It allows only
// same-origin assets; the PWA manifest and service worker are same-origin too.
const ContentSecurityPolicy = "default-src 'self'; " +
"img-src 'self' data:; " +
"style-src 'self'; " +
"script-src 'self'; " +
"connect-src 'self'; " +
"object-src 'none'; " +
"base-uri 'none'; " +
"form-action 'self'; " +
"frame-ancestors 'none'"
// SetSecurityHeaders writes the baseline security headers on every response
// (SPEC-003 §4). HSTS is set only when the app is served over https (secure).
func SetSecurityHeaders(w http.ResponseWriter, secure bool) {
h := w.Header()
h.Set("X-Content-Type-Options", "nosniff")
h.Set("Referrer-Policy", "no-referrer")
h.Set("X-Frame-Options", "DENY")
h.Set("Content-Security-Policy", ContentSecurityPolicy)
// L4s — the isolation trio: deny the powerful features the app never uses,
// and keep the document and its subresources same-origin (so a compromised
// or mis-hosted sibling cannot open, embed, or read MyDay).
h.Set("Permissions-Policy", PermissionsPolicy)
h.Set("Cross-Origin-Opener-Policy", "same-origin")
h.Set("Cross-Origin-Resource-Policy", "same-origin")
if secure {
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
}
// PermissionsPolicy disables the browser features MyDay does not use (L4s).
// An empty allowlist means "no origin, including self, may use it".
const PermissionsPolicy = "accelerometer=(), camera=(), geolocation=(), " +
"gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"