feat: Content-Security-Policy im Report-Only-Modus einfuehren
Some checks are pending
Playwright Tests / test (push) Waiting to run
Test and SonarQube / quality (push) Waiting to run

L4 aus dem TODO-Block Sicherheitsdurchgang. Bewusst vorsichtig: die Policy laeuft
zunaechst nur als "Content-Security-Policy-Report-Only" und blockiert nichts, meldet
aber ueber den neuen Endpunkt POST /api/csp-report jeden Verstoss (console.error/PM2).

Whitelist aus dem tatsaechlichen Code abgeleitet, nicht geraten:
- fonts.googleapis.com/fonts.gstatic.com: FontPicker.tsx/WeeklyView.tsx laden
  Nutzer-Schriftarten zur Laufzeit per <link> nach
- 'unsafe-inline' fuer script-src/style-src: Next.js' App-Router-Hydration und der
  eine dangerouslySetInnerHTML-Block in WeeklyView.tsx (View-Transition-Styles)
  brauchen das ohne Nonce-Infrastruktur (eigener, groesserer Umbau)
- Kein <iframe>, keine Drittanbieter-Bilder, keine clientseitigen fetch()-Aufrufe zu
  fremden Domains gefunden. FontAwesome laeuft als React-Komponente im eigenen
  Bundle. OAuth-Redirects sind Top-Level-Navigation, von CSP nicht betroffen.

src/middleware.ts: /api/csp-report von der Login-Pflicht ausgenommen -- der Browser
schickt Reports automatisch, auch von der nicht angemeldeten Login-Seite.

Scharf schalten (Report-Only aus dem Header-Namen entfernen) erst, nachdem ein paar
Tage Reports durchgesehen wurden -- die OAuth-Flows (Google/Microsoft/Apple/Notion)
liessen sich hier nicht interaktiv durchklicken.

Verifiziert: Typecheck, Lint, 85 Unit-Tests, Produktions-Build, Header und
Report-Endpunkt manuell im gebauten Standalone-Server geprueft.

v1.125.0

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-12 17:43:48 +02:00
parent 158051bb47
commit a20cc6dc65
5 changed files with 87 additions and 8 deletions

View File

@ -2,10 +2,22 @@
All notable changes to this project are documented in this file. All notable changes to this project are documented in this file.
## [1.124.0] - noch nicht deployed ## [1.125.0] - 2026-09-12
**Achtung: dieser Commit braucht `CREDENTIAL_ENCRYPTION_KEY` in der Produktions-`.env`, ### Security
bevor er deployed wird — siehe unten.** - **Content-Security-Policy ergänzt, vorerst als `Content-Security-Policy-Report-Only`.**
Blockiert nichts, meldet aber jeden Verstoß an den neuen Endpunkt `POST /api/csp-report`
(loggt über `console.error`/PM2). Die Whitelist ist aus dem tatsächlichen Code abgeleitet:
`fonts.googleapis.com`/`fonts.gstatic.com` für zur Laufzeit nachgeladene Schriftarten
(`FontPicker.tsx`, `WeeklyView.tsx`), `'unsafe-inline'` für Next.js' App-Router-Hydration
und den einen `dangerouslySetInnerHTML`-Block. Kein `<iframe>`, keine Drittanbieter-Bilder,
keine clientseitigen Fetches zu fremden Domains gefunden. Scharf geschaltet wird erst,
nachdem ein paar Tage Reports durchgesehen wurden — insbesondere die OAuth-Login-Flows
ließen sich hier nicht interaktiv durchklicken.
## [1.124.0] - 2026-09-12
**`CREDENTIAL_ENCRYPTION_KEY` wurde vor dem Deploy in der Produktions-`.env` gesetzt.**
### Security ### Security
- **Apple-/Synology-Kalenderpasswörter und OAuth-Tokens werden jetzt verschlüsselt - **Apple-/Synology-Kalenderpasswörter und OAuth-Tokens werden jetzt verschlüsselt

View File

@ -43,10 +43,40 @@ const nextConfig = {
serverComponentsExternalPackages: ['ical.js', 'tsdav'], serverComponentsExternalPackages: ['ical.js', 'tsdav'],
}, },
// Standard-Sicherheitsheader, die von Scannern (securityheaders.com, Mozilla Observatory, // Standard-Sicherheitsheader, die von Scannern (securityheaders.com, Mozilla Observatory,
// OWASP ZAP Baseline) sonst als fehlend markiert werden. Bewusst ohne Content-Security-Policy: // OWASP ZAP Baseline) sonst als fehlend markiert werden.
// die brauchte eine sorgfaeltige Whitelist (Google/Microsoft-OAuth, Google Fonts, FontAwesome- //
// CDN, inline Styles in WeeklyView) und eigene Tests, um nichts zu zerbrechen -- eigener Schritt. // Content-Security-Policy laeuft bewusst nur als Report-Only: sie blockiert nichts, meldet
// aber ueber /api/csp-report, was eine scharfe Policy blockieren wuerde. Die Whitelist ist
// aus dem tatsaechlichen Code abgeleitet (12.09.2026):
// - script-src/style-src 'unsafe-inline': Next.js' App-Router-Hydration-Skripte und der
// eine dangerouslySetInnerHTML-Block in WeeklyView.tsx (View-Transition-Styles) brauchen
// das ohne Nonce-Infrastruktur (eigener, groesserer Umbau via Middleware).
// - style-src/font-src fonts.googleapis.com/fonts.gstatic.com: FontPicker.tsx und
// WeeklyView.tsx laden Nutzer-Schriftarten zur Laufzeit per <link>-Tag nach.
// - Keine weiteren externen Domains gefunden: kein <iframe>, keine Bilder von
// Drittanbietern (Profilbilder werden nirgends gerendert), keine Client-seitigen
// fetch()-Aufrufe zu Fremd-Domains (Notion-OAuth laeuft serverseitig in einer API-Route).
// Erst nach ein paar Tagen Reports pruefen -- vor allem die OAuth-Login-Flows (Google/
// Microsoft/Apple/Notion) liessen sich hier nicht interaktiv durchklicken -- scharf schalten:
// "-Report-Only" aus dem Header-Namen entfernen und report-uri optional behalten.
async headers() { async headers() {
const csp = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com data:",
"img-src 'self' data: blob:",
"connect-src 'self'",
"frame-src 'none'",
"frame-ancestors 'none'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"manifest-src 'self'",
"worker-src 'self'",
"report-uri /api/csp-report",
].join('; ');
return [ return [
{ {
source: '/:path*', source: '/:path*',
@ -55,6 +85,7 @@ const nextConfig = {
{ key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{ key: 'Content-Security-Policy-Report-Only', value: csp },
], ],
}, },
]; ];

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.124.0", "version": "1.125.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from 'next/server';
import { rateLimit, clientIp } from '@/lib/rate-limit';
export const dynamic = 'force-dynamic';
/**
* Sammelstelle für Content-Security-Policy-Verstöße.
*
* Solange die Policy nur als `Content-Security-Policy-Report-Only` läuft (siehe
* next.config.js), blockiert sie nichts -- sie meldet nur, was eine scharfe Policy
* blockieren würde. Ohne diesen Endpunkt läge das nur im Browser-DevTools jedes einzelnen
* Nutzers, nirgends zusammengeführt. Bewusst ungeschützt (Browser rufen das ohne Session
* auf) und bewusst großzügig geloggt, bis genug reale Daten für eine scharfe Policy da sind.
*/
export async function POST(request: NextRequest) {
// Browser können das bei einem verbreiteten Verstoß massenhaft gleichzeitig auslösen --
// pro IP drosseln, damit das nicht die Logs flutet oder als DoS-Vektor auf diese Route
// selbst missbraucht werden kann.
const limit = rateLimit(`csp-report:${clientIp(request)}`, 20, 60 * 1000);
if (!limit.ok) return new NextResponse(null, { status: 204 });
try {
const body = await request.text();
if (body) {
console.warn('[CSP-REPORT]', body.slice(0, 4000));
}
} catch {
// Malformte Reports sind kein Fehlerfall, den ein Client je sehen muss.
}
return new NextResponse(null, { status: 204 });
}

View File

@ -9,7 +9,11 @@ export async function middleware(request: NextRequest) {
request.nextUrl.pathname.startsWith('/_next') || request.nextUrl.pathname.startsWith('/_next') ||
request.nextUrl.pathname === '/favicon.ico' || request.nextUrl.pathname === '/favicon.ico' ||
request.nextUrl.pathname === '/sw.js' || request.nextUrl.pathname === '/sw.js' ||
request.nextUrl.pathname === '/manifest.webmanifest' request.nextUrl.pathname === '/manifest.webmanifest' ||
// Der Browser schickt CSP-Verstoss-Reports automatisch, auch von der (nicht
// angemeldeten) Login-Seite -- ohne Ausnahme wuerde die Middleware sie zur
// Login-Seite umleiten statt an den Report-Endpunkt.
request.nextUrl.pathname === '/api/csp-report'
) { ) {
return NextResponse.next(); return NextResponse.next();
} }