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>
49 lines
1.9 KiB
TypeScript
49 lines
1.9 KiB
TypeScript
import type { Handle } from '@sveltejs/kit';
|
|
|
|
/**
|
|
* Server-Hook für Security-Headers auf jeder ausgelieferten Response.
|
|
*
|
|
* 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');
|
|
}
|
|
return response;
|
|
};
|