Zwei Kommentare versprachen Schutz, den es nicht gab. Das ist schlimmer als eine fehlende Zusage, weil man sich darauf verlaesst: - In middleware.ts stand "Ohne Authentifizierungs-Cookie gar nicht erst weiterreichen". Die Middleware prueft keine Anmeldung, sie setzt Kopfzeilen; die Liste steuert allein den Cache-Control-Kopf. Wer den Satz glaubt, verzichtet an einer neuen Seite auf erfordereBenutzer(). Die Liste heisst jetzt OHNE_CACHE_SPERRE und sagt selbst, was sie ist. - In datei.ts hiess es, die Pfadpruefung fange "Symlink-Tricks" ab. resolve() rechnet mit Zeichenketten und folgt keinem Link - dafuer braeuchte es realpath(). Nachgeruestet wird es nicht: Wer dort einen Symlink anlegen kann, hat schon Schreibrechte im Datenverzeichnis. Der Satz musste trotzdem weg. Dazu docs/SICHERHEIT.md: Bedrohungsmodell, der Durchgang entlang der OWASP Top 10 - und ein eigener Abschnitt darueber, was ausdruecklich NICHT geschuetzt ist. Aus dem OWASP-Durchgang kamen keine weiteren Funde. Zwei Stellen sahen danach aus und waren es nicht: Prisma.join() in suche.ts fuegt nur bereits parametrisierte Fragmente zusammen, und der Suchausschnitt mit den <mark>-Markern aus ts_headline wird zerlegt und als React-Kinder gerendert, nicht als HTML - obwohl die Quelle der OCR-Text hochgeladener Dokumente ist. Refs #38
118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
import { NextResponse, type NextRequest } from 'next/server'
|
|
|
|
/**
|
|
* Content-Security-Policy mit Nonce.
|
|
*
|
|
* Warum hier und nicht in next.config? Weil die Nonce pro Anfrage neu sein
|
|
* muss. Statische Header koennen das nicht.
|
|
*
|
|
* Die Richtlinie ist bewusst eng: keine fremden Quellen, keine Fonts von
|
|
* Google, kein CDN, keine Analytics. Was nicht aus dieser Anwendung kommt,
|
|
* darf nicht geladen werden - hier liegen Gesundheits- und Finanzdaten.
|
|
*
|
|
* Im Produktivbetrieb gilt 'strict-dynamic': Next.js haengt die Nonce an
|
|
* seine eigenen Skript-Tags, und diese duerfen dann weitere eigene
|
|
* Skripte nachladen. Kein Fremdskript kommt durch, auch nicht ueber eine
|
|
* eingeschleuste <script src>-Zeile.
|
|
*
|
|
* Im Entwicklungsmodus geht das NICHT: 'strict-dynamic' schaltet die
|
|
* Freigabe nach Herkunft ab, also auch 'self'. Der Hot-Reload und die
|
|
* Fehleranzeige von Next.js laden aber Skripte ohne Nonce
|
|
* (webpack.js, react-refresh.js, das Fehler-Overlay des Pages-Routers).
|
|
* Die wuerden alle blockiert - man saehe eine tote Seite statt der
|
|
* Fehlermeldung. Deshalb hier bewusst die lockere Fassung.
|
|
*
|
|
* Weitere bewusste Zugestaendnisse:
|
|
* - style-src erlaubt 'unsafe-inline'. Tailwind selbst braucht das nicht,
|
|
* aber Next.js und Radix setzen Inline-Styles fuer Positionierung. Das
|
|
* Risiko ist bei Styles deutlich kleiner als bei Skripten.
|
|
* - img-src erlaubt data: - QR-Codes und Belegvorschauen werden als
|
|
* Data-URI eingebettet, statt sie ueber einen Fremddienst zu holen.
|
|
*/
|
|
|
|
/**
|
|
* Seiten, die auch ohne Anmeldung sinnvoll im Verlauf stehen duerfen.
|
|
*
|
|
* ACHTUNG - was diese Liste NICHT ist: eine Zugriffskontrolle. Frueher
|
|
* stand hier "Ohne Authentifizierungs-Cookie gar nicht erst weiterreichen",
|
|
* und das war schlicht falsch: Die Middleware prueft keine Anmeldung, sie
|
|
* setzt Kopfzeilen. Ausgewertet wird die Liste einzig fuer die Frage, ob
|
|
* "Cache-Control: no-store" gesetzt wird.
|
|
*
|
|
* Der Irrtum waere teuer geworden: Wer den Satz glaubt, verzichtet an einer
|
|
* neuen Seite auf erfordereBenutzer() - "die Middleware faengt das ja ab".
|
|
* Sie faengt nichts ab. Der Zugriffsschutz sitzt in
|
|
* src/lib/auth/sitzung.ts, und nur dort.
|
|
*/
|
|
const OHNE_CACHE_SPERRE = [
|
|
'/anmelden',
|
|
'/ersteinrichtung',
|
|
'/datenschutz',
|
|
'/impressum',
|
|
'/api/auth',
|
|
]
|
|
|
|
function nonceErzeugen(): string {
|
|
const bytes = new Uint8Array(16)
|
|
crypto.getRandomValues(bytes)
|
|
return btoa(String.fromCharCode(...bytes))
|
|
}
|
|
|
|
export function middleware(anfrage: NextRequest) {
|
|
const nonce = nonceErzeugen()
|
|
const istEntwicklung = process.env.NODE_ENV === 'development'
|
|
|
|
const richtlinie = [
|
|
`default-src 'self'`,
|
|
istEntwicklung
|
|
? `script-src 'self' 'unsafe-inline' 'unsafe-eval'`
|
|
: `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
|
|
`style-src 'self' 'unsafe-inline'`,
|
|
`img-src 'self' blob: data:`,
|
|
`font-src 'self'`,
|
|
// Uploads und Server Actions gehen an die eigene Herkunft. Im
|
|
// Entwicklungsmodus zusaetzlich der WebSocket des Hot-Reloads.
|
|
`connect-src 'self'${istEntwicklung ? ' ws: wss:' : ''}`,
|
|
`media-src 'self' blob:`,
|
|
`object-src 'none'`,
|
|
`base-uri 'self'`,
|
|
`form-action 'self'`,
|
|
`frame-ancestors 'none'`,
|
|
`frame-src 'self' blob:`,
|
|
`worker-src 'self' blob:`,
|
|
`manifest-src 'self'`,
|
|
...(istEntwicklung ? [] : ['upgrade-insecure-requests']),
|
|
].join('; ')
|
|
|
|
const kopf = new Headers(anfrage.headers)
|
|
kopf.set('x-nonce', nonce)
|
|
|
|
const antwort = NextResponse.next({ request: { headers: kopf } })
|
|
antwort.headers.set('Content-Security-Policy', richtlinie)
|
|
|
|
// Verhindert, dass ein Bericht oder Beleg im Verlauf des Browsers
|
|
// zwischengespeichert wird.
|
|
if (!OHNE_CACHE_SPERRE.some((pfad) => anfrage.nextUrl.pathname.startsWith(pfad))) {
|
|
antwort.headers.set('Cache-Control', 'no-store, must-revalidate')
|
|
}
|
|
|
|
return antwort
|
|
}
|
|
|
|
export const config = {
|
|
matcher: [
|
|
/*
|
|
* Alles ausser statischen Dateien und dem Favicon. Der negative
|
|
* Lookahead haelt die Middleware von Auslieferungspfaden fern, wo sie
|
|
* nur Rechenzeit kosten wuerde.
|
|
*/
|
|
{
|
|
source: '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|jpeg|svg|webp|ico|woff2?)$).*)',
|
|
missing: [
|
|
{ type: 'header', key: 'next-router-prefetch' },
|
|
{ type: 'header', key: 'purpose', value: 'prefetch' },
|
|
],
|
|
},
|
|
],
|
|
}
|