security(cards): CSP report-only + service-key rotation playbook
Some checks are pending
CI / validate (push) Waiting to run

Folge-Hardening zu e1ddbf3, Cluster A2+A3 aus FEATURE_IDEAS.

* hooks.server.ts: restriktive CSP im Report-Only-Modus
  (default-src 'self', script-src 'self', connect-src whitelist
  auf cardecky-api/auth.mana.how/share/mcp). CARDS_CSP_ENFORCE=true
  flippt auf den scharfen Header.
* docs/playbooks/SERVICE_KEY_ROTATION.md: 5-Schritt-Rotation für
  CARDS_DSGVO_SERVICE_KEY bis Phase F-1 (mana-auth-managed Keys).

Forensik der Bypass-Periode 2026-05-08 → 2026-05-12 ist abgeschlossen:
nur 2 user_ids in der Cards-DB, beide legitim (tills95@gmail.com +
Smoke-Test-Sentinel c1a5, letztere via DSGVO-Endpoint aufgeräumt).
Kein ausgenutzter Bypass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-12 18:40:29 +02:00
parent e1ddbf34b3
commit 5a29dd9a8c
3 changed files with 164 additions and 14 deletions

View file

@ -3,21 +3,45 @@ import type { Handle } from '@sveltejs/kit';
/**
* Server-Hook für Security-Headers auf jeder ausgelieferten Response.
*
* Bewusst minimalistisch: nur die well-known Header, die ohne
* Browser-Verhaltens-Test sicher sind. **CSP fehlt absichtlich**
* eine richtige Content-Security-Policy braucht App-Inventur
* (inline-styles, externe Theme-Assets, Markdown-Renderer) und
* sollte separat mit Live-Test eingeführt werden.
*
* Cloudflare-Tunnel-Transform-Rules dürfen das gerne nochmal
* obendrauf setzen die hier sind die Defense-Tiefe-Schicht für
* den Fall, dass die Cloudflare-Schiene aus dem Pfad fällt.
*
* CSP läuft initial im **Report-Only-Mode**: Browser melden
* Violations in die DevTools-Console (`console.warn`), die App
* bleibt funktional. Nach 1-2 Tagen Live-Beobachtung muss die
* Policy mit echten Reports getunet und auf Enforce geflippt
* werden `CARDS_CSP_ENFORCE=true` aktiviert den scharfen Header.
*/
const CSP_POLICY = [
"default-src 'self'",
// 'unsafe-inline' für Svelte-5-Style-Attribute + Theme-CSS-Variables;
// langfristig durch nonce-/hash-basierten Style ersetzen.
"style-src 'self' 'unsafe-inline'",
"script-src 'self'",
"img-src 'self' data: blob:",
"font-src 'self' data:",
// XHR/fetch-Ziele: eigene API + Auth-Portal + LLM/Share/MCP-Plattform.
"connect-src 'self' https://cardecky-api.mana.how https://auth.mana.how https://share.mana.how https://mcp.mana.how",
"media-src 'self' blob:",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self' https://auth.mana.how",
"object-src 'none'",
].join('; ');
export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event);
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
const cspHeader =
process.env.CARDS_CSP_ENFORCE === 'true'
? 'Content-Security-Policy'
: 'Content-Security-Policy-Report-Only';
response.headers.set(cspHeader, CSP_POLICY);
if (process.env.NODE_ENV === 'production') {
response.headers.set('Strict-Transport-Security', 'max-age=15552000; includeSubDomains');
}