Files
betreuungs-cockpit/src/middleware.ts
T
Florian Wilde e48eda4332 Irrefuehrende Sicherheitszusagen entfernen, OWASP-Durchgang festhalten
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
2026-08-24 09:53:43 +02:00

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' },
],
},
],
}