waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
| 1 | /** |
| 2 | * Content-Security-Policy – eine einzige Quelle der Wahrheit. |
| 3 | * |
| 4 | * Die Richtlinie erreicht den Renderer auf zwei Wegen, und beide gelten |
| 5 | * gleichzeitig. Chromium wertet mehrere Richtlinien nebeneinander aus, jede |
| 6 | * für sich; erlaubt bleibt nur, was allen genügt – die Schnittmenge. |
| 7 | * |
| 8 | * 1. als `<meta http-equiv="Content-Security-Policy">` im Renderer-HTML |
| 9 | * (eingesetzt beim Bau, siehe `cspPlugin` in `electron.vite.config.ts`), |
| 10 | * 2. als HTTP-Kopf über `onHeadersReceived` im Main-Prozess |
| 11 | * (siehe `cspHeaderSetzen` in `src/main/sicherheit.ts`). |
| 12 | * |
| 13 | * ## Warum es zwei Fassungen gibt |
| 14 | * |
| 15 | * Drei Direktiven ignoriert jeder Browser, wenn sie über ein `<meta>`-Element |
| 16 | * kommen: `frame-ancestors`, `report-uri` und `sandbox`. Sie richten sich an |
| 17 | * den einbettenden Kontext bzw. an den Ladevorgang selbst – beides steht fest, |
| 18 | * bevor das HTML geparst ist. Chromium schreibt dann bei jedem Start einen |
| 19 | * Konsolenfehler: „The Content Security Policy directive 'frame-ancestors' is |
| 20 | * ignored when delivered via a <meta> element.“ |
| 21 | * |
| 22 | * Genau das tat dieses Programm, weil `frame-ancestors 'none'` in beiden |
| 23 | * Fassungen stand. Eine Direktive, die dasteht und nicht gilt, ist schlimmer |
| 24 | * als keine: Zwei Tests prüften ihren Wortlaut und meldeten grün. |
| 25 | * |
| 26 | * Die `<meta>`-Fassung lässt diese Direktiven deshalb weg ({@link NUR_IM_KOPF}). |
| 27 | * Im Kopf bleiben sie stehen – dort gelten sie. |
| 28 | * |
| 29 | * ## Warum der Schutz dabei nicht verloren geht – nachgemessen |
| 30 | * |
| 31 | * Hier stand früher, der Kopf-Weg sei nur für den Vite-Dev-Server da und |
| 32 | * `file://` hänge allein am `<meta>`-Element. Das stimmt nicht: |
| 33 | * `onHeadersReceived` greift in Electron auch für `file://`-Antworten. |
| 34 | * |
| 35 | * Nachgemessen am gebauten Programm (`out/main/index.js`, Electron 43): Ein |
| 36 | * absichtlich ausgelöster Verstoß gegen `img-src` meldete **zwei** |
| 37 | * `securitypolicyviolation`-Ereignisse – eines mit dem Wortlaut der |
| 38 | * Kopf-Fassung, eines mit dem der `<meta>`-Fassung. Der Kopf kommt also im |
| 39 | * Renderer an, und mit ihm `frame-ancestors`. Dieselbe Messung führt |
| 40 | * `e2e/anwendung.spec.ts` bei jedem Lauf im echten Fenster durch, damit die |
| 41 | * Zusicherung nicht wieder zur Behauptung wird. |
| 42 | * |
| 43 | * ## Beide Fassungen dürfen sonst nicht auseinanderlaufen |
| 44 | * |
| 45 | * Wäre die Kopf-Fassung strenger als die `<meta>`-Fassung, bräche der |
| 46 | * Dev-Modus unbemerkt an der Schnittmenge. `tests/sicherheit.test.ts` hält |
| 47 | * deshalb fest: `<meta>`-Fassung == Kopf-Fassung ohne die Nur-Kopf-Direktiven, |
| 48 | * Zeichen für Zeichen. |
| 49 | */ |
| 50 | |
| 51 | export type CspModus = 'development' | 'production'; |
| 52 | |
| 53 | /** Über welchen Weg die Richtlinie ausgeliefert wird. */ |
| 54 | export type CspZiel = 'meta' | 'kopf'; |
| 55 | |
| 56 | /** |
| 57 | * Direktiven, die ausschließlich als HTTP-Kopf wirken. |
| 58 | * |
| 59 | * Über ein `<meta>`-Element werden sie ignoriert – mit Konsolenfehler. Wer |
| 60 | * hier etwas ergänzt, nimmt es damit zugleich aus der `<meta>`-Fassung heraus. |
| 61 | * Vollständig laut CSP Level 3: `frame-ancestors`, `report-uri`, `sandbox`. |
| 62 | * Aufgeführt sind nur die, die diese Anwendung tatsächlich setzt. |
| 63 | */ |
| 64 | const NUR_IM_KOPF: readonly string[] = ['frame-ancestors']; |
| 65 | |
| 66 | /** |
| 67 | * Im Produktionsbuild gilt eine strenge Richtlinie ohne `unsafe-*`. |
| 68 | * Im Dev-Modus sind zusätzlich nötig: |
| 69 | * - `'unsafe-inline'` für Skripte: Vite injiziert die React-Refresh-Präambel |
| 70 | * als Inline-Skript in das HTML. |
| 71 | * - `'unsafe-inline'` für Styles: HMR fügt `<style>`-Elemente zur Laufzeit ein. |
| 72 | * - `ws:` in `connect-src` für den HMR-WebSocket. |
| 73 | * |
| 74 | * `ziel` ist mit Absicht ein Pflichtargument: Jede Aufrufstelle muss sagen, |
| 75 | * über welchen Weg sie ausliefert – ein stiller Vorgabewert hätte die |
| 76 | * `<meta>`-Fassung genau so wieder mit `frame-ancestors` beliefert. |
| 77 | */ |
| 78 | export function cspRichtlinie(modus: CspModus, ziel: CspZiel): string { |
| 79 | const entwicklung = modus === 'development'; |
| 80 | |
| 81 | const direktiven: Record<string, readonly string[]> = { |
| 82 | 'default-src': ["'none'"], |
| 83 | 'script-src': entwicklung ? ["'self'", "'unsafe-inline'"] : ["'self'"], |
| 84 | 'style-src': entwicklung ? ["'self'", "'unsafe-inline'"] : ["'self'"], |
| 85 | 'img-src': ["'self'", 'data:'], |
| 86 | 'font-src': ["'self'"], |
| 87 | 'media-src': ["'self'"], |
| 88 | 'connect-src': entwicklung ? ["'self'", 'ws:', 'http://localhost:*'] : ["'self'"], |
| 89 | 'worker-src': ["'self'"], |
| 90 | 'manifest-src': ["'self'"], |
| 91 | 'base-uri': ["'none'"], |
| 92 | 'form-action': ["'none'"], |
| 93 | /* Wirkt auch als `<meta>`: verbietet der eigenen Seite, irgendetwas |
| 94 | einzubetten. Nicht zu verwechseln mit `frame-ancestors`, das umgekehrt |
| 95 | das Eingebettetwerden verbietet – und nur im Kopf gilt. */ |
| 96 | 'frame-src': ["'none'"], |
| 97 | 'frame-ancestors': ["'none'"], |
| 98 | 'object-src': ["'none'"], |
| 99 | }; |
| 100 | |
| 101 | return Object.entries(direktiven) |
| 102 | .filter(([direktive]) => ziel === 'kopf' || !NUR_IM_KOPF.includes(direktive)) |
| 103 | .map(([direktive, werte]) => `${direktive} ${werte.join(' ')}`) |
| 104 | .join('; '); |
| 105 | } |
| 106 | |
| 107 | /** Platzhalter in `src/renderer/index.html`, den das Vite-Plugin ersetzt. */ |
| 108 | export const CSP_PLATZHALTER = '%CSP%'; |