feat: Content-Security-Policy im Report-Only-Modus einfuehren
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:
parent
158051bb47
commit
a20cc6dc65
18
CHANGELOG.md
18
CHANGELOG.md
@ -2,10 +2,22 @@
|
||||
|
||||
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`,
|
||||
bevor er deployed wird — siehe unten.**
|
||||
### Security
|
||||
- **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
|
||||
- **Apple-/Synology-Kalenderpasswörter und OAuth-Tokens werden jetzt verschlüsselt
|
||||
|
||||
@ -43,10 +43,40 @@ const nextConfig = {
|
||||
serverComponentsExternalPackages: ['ical.js', 'tsdav'],
|
||||
},
|
||||
// Standard-Sicherheitsheader, die von Scannern (securityheaders.com, Mozilla Observatory,
|
||||
// OWASP ZAP Baseline) sonst als fehlend markiert werden. Bewusst ohne Content-Security-Policy:
|
||||
// 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.
|
||||
// OWASP ZAP Baseline) sonst als fehlend markiert werden.
|
||||
//
|
||||
// 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() {
|
||||
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 [
|
||||
{
|
||||
source: '/:path*',
|
||||
@ -55,6 +85,7 @@ const nextConfig = {
|
||||
{ key: 'X-Frame-Options', value: 'DENY' },
|
||||
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
||||
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
||||
{ key: 'Content-Security-Policy-Report-Only', value: csp },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
32
src/app/api/csp-report/route.ts
Normal file
32
src/app/api/csp-report/route.ts
Normal 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 });
|
||||
}
|
||||
@ -9,7 +9,11 @@ export async function middleware(request: NextRequest) {
|
||||
request.nextUrl.pathname.startsWith('/_next') ||
|
||||
request.nextUrl.pathname === '/favicon.ico' ||
|
||||
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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user