From fceec19dedfffa8a448c552932c20929675b3556 Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 9 Oct 2026 20:29:07 +0200 Subject: [PATCH] =?UTF-8?q?fix(quick-261009-p0m):=20Schutz-Kopfzeilen=20in?= =?UTF-8?q?=20der=20Weboberfl=C3=A4che,=20X-Powered-By=20abgeschaltet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - next.config.ts: nosniff, Referrer-Policy, X-Frame-Options SAMEORIGIN, CSP nur frame-ancestors 'self', Permissions-Policy (Kamera, Mikrofon, Standort, Zahlung, USB), COOP same-origin-allow-popups, poweredByHeader aus - API: X-Powered-By (Express) in configureHttp abgeschaltet - Tests: next-config.test.ts, http-setup.spec.ts - Sicherheitsprotokoll (Zeilen der Außenprüfung), Entwicklungsanleitung, CHANGELOG Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + apps/api/src/http-setup.spec.ts | 11 ++++++++- apps/api/src/http-setup.ts | 11 +++++++-- apps/api/src/main.ts | 3 ++- apps/web/next.config.ts | 28 ++++++++++++++++++++++ apps/web/src/next-config.test.ts | 40 ++++++++++++++++++++++++++++++++ docs/anleitung-entwicklung.md | 14 +++++++++++ docs/sicherheitsprotokoll.md | 13 +++++++---- 8 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/next-config.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7770422..f2dd50d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T - Nextcloud-Status: Über der Kachelliste gibt es jetzt ein Suchfeld. Es zeigt nur die Clouds, deren Kundenname den eingegebenen Text enthält; Groß- und Kleinschreibung spielen keine Rolle, die gewählte Sortierung bleibt erhalten. Die Kacheln sind kompakter, der Kundenname und die Adresse der Cloud stehen in voller Länge da, statt abgeschnitten zu werden. Die Knöpfe Benachrichtigen, Prüfen und Bearbeiten sitzen jetzt unten rechts neben „Zuletzt geprüft“. - Nextcloud-Status: Als Logo können Sie jetzt auch eine Bildadresse eingeben, die mit http:// beginnt. Tessera holt das Bild beim Speichern einmalig ab und behält es wie ein hochgeladenes Bild (höchstens 1 MB; PNG, JPEG, GIF oder WebP). Adressen, die nur intern erreichbar sind, holt Tessera nicht ab – solche Bilder laden Sie über „Bild hochladen“ hoch. Adressen mit https:// lädt weiterhin direkt Ihr Browser. Das Feld heißt jetzt „Bildadresse“. - Sicherheit: Die ausgelieferten Server-Abbilder sind kleiner und enthalten keine Entwicklungswerkzeuge und keine Paketverwaltung mehr (das Server-Abbild fiel von 1,59 auf 1,12 GB, die Zahl bekannter Schwachstellen darin deutlich). Für Sie ändert sich im Alltag nichts; wer Befehle direkt im Container ausführt, nutzt dafür `node` statt `pnpm` oder `npm`. +- Sicherheit: Tessera schickt jetzt selbst Schutz-Kopfzeilen an den Browser, zum Beispiel gegen das Einrahmen der Anmeldeseite durch fremde Seiten, und verrät in den Antworten nicht mehr, mit welchem Programm der Server gebaut ist. Kamera, Mikrofon und Standort sind für Tessera-Seiten gesperrt; „Link kopieren“, neue Fenster (auch die Nextcloud-Anmeldung) und eingebettete Kacheln funktionieren unverändert. ### Behoben diff --git a/apps/api/src/http-setup.spec.ts b/apps/api/src/http-setup.spec.ts index 67f6623..df6bb53 100644 --- a/apps/api/src/http-setup.spec.ts +++ b/apps/api/src/http-setup.spec.ts @@ -14,6 +14,9 @@ function recordingApp() { enableCors: vi.fn(() => { calls.push('cors'); }), + disable: vi.fn((name: string) => { + calls.push(`disable:${name}`); + }), } as unknown as HttpApp; return { app, calls }; } @@ -38,6 +41,12 @@ describe('configureHttp (Review WR-01)', () => { it('Anfragelog und Cookies kommen vor allem anderen', () => { const { app, calls } = recordingApp(); configureHttp(app, 'x'); - expect(calls.slice(0, 2)).toEqual(['use', 'use']); + expect(calls.filter((c) => !c.startsWith('disable:')).slice(0, 2)).toEqual(['use', 'use']); + }); + + it('schaltet die Kopfzeile X-Powered-By ab', () => { + const { app } = recordingApp(); + configureHttp(app, 'x'); + expect(app.disable).toHaveBeenCalledWith('x-powered-by'); }); }); diff --git a/apps/api/src/http-setup.ts b/apps/api/src/http-setup.ts index b0933f9..800bfbb 100644 --- a/apps/api/src/http-setup.ts +++ b/apps/api/src/http-setup.ts @@ -1,4 +1,5 @@ -import { type INestApplication, ValidationPipe } from '@nestjs/common'; +import { ValidationPipe } from '@nestjs/common'; +import type { NestExpressApplication } from '@nestjs/platform-express'; import cookieParser from 'cookie-parser'; import { CERT_BUILD_ROUTE, @@ -8,7 +9,10 @@ import { import { requestLogMiddleware } from './common/request-log'; /** Der Teil der Nest-Anwendung, den die HTTP-Einrichtung braucht (so laesst sich die Reihenfolge testen). */ -export type HttpApp = Pick; +export type HttpApp = Pick< + NestExpressApplication, + 'use' | 'useGlobalPipes' | 'enableCors' | 'disable' +>; /** * Gemeinsame HTTP-Einrichtung der API (aus main.ts, damit die Reihenfolge testbar ist). @@ -21,6 +25,9 @@ export type HttpApp = Pick(AppModule); const configService = app.get(ConfigService); configureHttp(app, configService.get('CORS_ORIGIN', 'http://localhost:3000')); diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 464691a..b472afe 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -28,11 +28,39 @@ function readChangelog(): string { } } +// Schutz-Kopfzeilen der Anwendung (quick-261009-p0m, aus dem ersten ZAP-Lauf). Sie gelten fuer +// alle Pfade und stehen hier, damit sie auch ohne vorgeschalteten Proxy gesendet werden +// (doppelt gesichert; setzt der Proxy dieselben Kopfzeilen, gewinnt keine Seite etwas oder +// verliert etwas). Absichtlich KEINE vollstaendige Content-Security-Policy -- sie braucht eine +// eigene Pruefung der Inline-Skripte von Next.js und bleibt im Sicherheitsprotokoll "offen". +// `frame-ancestors 'self'` ist nur die eine Richtlinie gegen das Einrahmen durch fremde Seiten +// (Gegenstueck zu X-Frame-Options fuer neuere Browser). Eingebettete FREMDE Seiten (XFrame-Kachel, +// eigene Module) sind nicht betroffen: diese Kopfzeilen schuetzen nur Tessera selbst. +// Permissions-Policy sperrt Kamera, Mikrofon, Standort, Zahlung und USB; die Zwischenablage +// (clipboard-write fuer "Link kopieren"), Vollbild und Benachrichtigungen bleiben unberuehrt. +// Cross-Origin-Opener-Policy `same-origin-allow-popups`: jeder Link in ein neues Fenster +// (Nextcloud-Anmeldung, Favoriten, ...) traegt noopener, nichts nutzt window.opener/postMessage. +const securityHeaders = [ + { key: 'X-Content-Type-Options', value: 'nosniff' }, + { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, + { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, + { key: 'Content-Security-Policy', value: "frame-ancestors 'self'" }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=(), payment=(), usb=()', + }, + { key: 'Cross-Origin-Opener-Policy', value: 'same-origin-allow-popups' }, +]; + const nextConfig: NextConfig = { output: 'standalone' as const, + poweredByHeader: false, env: { TESSERA_CHANGELOG_MD: readChangelog(), }, + async headers() { + return [{ source: '/:path*', headers: securityHeaders }]; + }, async rewrites() { const apiUrl = process.env.API_INTERNAL_URL || 'http://api:3001'; return [ diff --git a/apps/web/src/next-config.test.ts b/apps/web/src/next-config.test.ts new file mode 100644 index 0000000..fc055e5 --- /dev/null +++ b/apps/web/src/next-config.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import nextConfig from '../next.config'; + +/** + * Schutz-Kopfzeilen der Weboberflaeche (quick-261009-p0m). Der Test haelt fest, dass sie fuer + * alle Pfade gelten und dass die Zwischenablage (clipboard-write fuer "Link kopieren") nicht + * gesperrt wird. + */ +describe('next.config: Schutz-Kopfzeilen', () => { + it('sendet keinen X-Powered-By', () => { + expect(nextConfig.poweredByHeader).toBe(false); + }); + + it('setzt die Kopfzeilen fuer alle Pfade', async () => { + const rules = await nextConfig.headers?.(); + expect(rules).toHaveLength(1); + expect(rules?.[0].source).toBe('/:path*'); + const h = Object.fromEntries((rules?.[0].headers ?? []).map((x) => [x.key, x.value])); + expect(h['X-Content-Type-Options']).toBe('nosniff'); + expect(h['Referrer-Policy']).toBe('strict-origin-when-cross-origin'); + expect(h['X-Frame-Options']).toBe('SAMEORIGIN'); + expect(h['Cross-Origin-Opener-Policy']).toBe('same-origin-allow-popups'); + }); + + it('beschraenkt die Content-Security-Policy auf frame-ancestors', async () => { + const rules = await nextConfig.headers?.(); + const csp = rules?.[0].headers.find((x) => x.key === 'Content-Security-Policy'); + expect(csp?.value).toBe("frame-ancestors 'self'"); + }); + + it('sperrt Kamera, Mikrofon, Standort, Zahlung und USB, aber nicht die Zwischenablage', async () => { + const rules = await nextConfig.headers?.(); + const pp = rules?.[0].headers.find((x) => x.key === 'Permissions-Policy')?.value ?? ''; + for (const f of ['camera', 'microphone', 'geolocation', 'payment', 'usb']) { + expect(pp).toContain(`${f}=()`); + } + expect(pp).not.toContain('clipboard'); + expect(pp).not.toContain('fullscreen'); + }); +}); diff --git a/docs/anleitung-entwicklung.md b/docs/anleitung-entwicklung.md index f029940..59f9446 100644 --- a/docs/anleitung-entwicklung.md +++ b/docs/anleitung-entwicklung.md @@ -986,6 +986,20 @@ laufen mit allen Abhängigkeiten und fangen es nicht. Dann gehört das Paket in Migrationen laufen, die Zeile „Tessera API running“ erscheint) plus die Rauchtests am neu gebauten Stack – so wie es `checks/fresh-db-start.sh` im Auftrag quick-261009-p0m vormacht. +**Schutz-Kopfzeilen der Weboberfläche (quick-261009-p0m):** `apps/web/next.config.ts` sendet für +alle Pfade `X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options: SAMEORIGIN`, eine +Content-Security-Policy, die **nur** `frame-ancestors 'self'` enthält (keine vollständige +Richtlinie – sie braucht eine eigene Prüfung der Inline-Skripte von Next.js), eine +`Permissions-Policy` (Kamera, Mikrofon, Standort, Zahlung, USB gesperrt) und +`Cross-Origin-Opener-Policy: same-origin-allow-popups`; `poweredByHeader` ist aus. **Vor jeder +Erweiterung prüfen:** Die Zwischenablage (`clipboard-write` für „Link kopieren“) darf nicht in der +Permissions-Policy gesperrt werden. Neue Fenster müssen weiter mit `noopener` geöffnet werden; +wer `window.opener` oder `postMessage` zwischen Fenstern braucht, muss die Opener-Richtlinie +überdenken. Eingebettete fremde Seiten (XFrame-Kachel, eigene Module) sind von diesen Kopfzeilen +nicht betroffen. Die API schaltet `X-Powered-By` in `configureHttp` ab (`apps/api/src/http-setup.ts`). +Geprüft werden die Kopfzeilen durch `apps/web/src/next-config.test.ts` und durch die Außenprüfung +(ZAP, siehe „Sicherheitsprüfungen“). + **NestJS-Routenreihenfolge:** NestJS matcht Routen in Deklarationsreihenfolge. Eine statische Route wie `@Get('source-config')` **muss vor** einem `@Get(':id')`-Platzhalter derselben Klasse stehen — sonst interpretiert der Platzhalter den literalen Pfadteil als `id` und "beschattet" die statische diff --git a/docs/sicherheitsprotokoll.md b/docs/sicherheitsprotokoll.md index 10c799c..68ecec9 100644 --- a/docs/sicherheitsprotokoll.md +++ b/docs/sicherheitsprotokoll.md @@ -14,7 +14,7 @@ Das Protokoll enthält bewusst keine Passwörter, keine Zugangsdaten und keine A - **Fremde Bausteine:** Die erste Prüfung verzeichnete 151 bekannte Schwachstellen in den Bausteinen anderer Hersteller, die Tessera im Betrieb verwendet, davon 5 kritische und 73 hohe. Am 9. Oktober 2026 wurden die Bausteine innerhalb ihrer bisherigen Versionslinien aktualisiert und zwei ungenutzte Bausteine entfernt; danach sind es 12 (0 kritische, 9 hohe, 3 mittlere). Die übrigen 12 haben keine bereinigte Fassung in der verwendeten Versionslinie (xlsx, node-forge) oder werden erst in einer neuen Hauptversion behoben (nodemailer, sharp, deepmerge-ts); sie sind unter „Einordnung der Befunde“ mit Grund eingeordnet. - **Eigener Quelltext:** Ein einziger echter Härtungspunkt wurde gefunden (die Längenprüfung bei der Entschlüsselung gespeicherter Geheimnisse, Schweregrad niedrig); er ist am 9. Oktober 2026 behoben und durch Tests abgesichert. Alles andere sind Fehlalarme oder bewusste, dokumentierte Entscheidungen. - **Automatische Prüfung:** Seit dem 9. Oktober 2026 läuft nach jedem Bau der Plattform automatisch eine Sicherheitsprüfung. Sie meldet nur und hält den Bau nie an. -- **Prüfung von außen:** Vor jeder Freigabe wird der Testserver alpha wie ein Besucher ohne Konto angesehen, rein passiv und ohne Angriffe. Der erste Lauf am 9. Oktober 2026 (direkt gegen die Anwendung, ohne den vorgeschalteten Proxy) fand nichts Hohes, 2 mittlere und 6 niedrige Hinweise sowie 3 reine Informationen; alle betreffen fehlende Schutz-Kopfzeilen der Weboberfläche. Der Lauf über die öffentliche Adresse steht noch aus. +- **Prüfung von außen:** Vor jeder Freigabe wird der Testserver alpha wie ein Besucher ohne Konto angesehen, rein passiv und ohne Angriffe. Der erste Lauf am 9. Oktober 2026 (direkt gegen die Anwendung, ohne den vorgeschalteten Proxy) fand nichts Hohes, 2 mittlere und 6 niedrige Hinweise sowie 3 reine Informationen; alle betreffen fehlende Schutz-Kopfzeilen der Weboberfläche. Die Anwendung sendet diese Kopfzeilen seit dem 9. Oktober 2026 selbst (bis auf eine vollständige Inhaltsrichtlinie, die offen bleibt). Der Lauf über die öffentliche Adresse steht noch aus und zeigt, ob auch der Proxy sie durchreicht. - **Bisherige Prüfungen:** Acht Code-Prüfungen größerer Änderungen mit zusammen 89 Befunden; 76 davon sind behoben, die übrigen 13 sind Hinweise der niedrigsten Stufe. Alle kritischen Befunde und alle Warnungen sind behoben. - **Was noch zu tun ist:** Die Abbilder enthalten seit dem 9. Oktober 2026 keine Entwicklungswerkzeuge und keine Paketverwaltung mehr (das Server-Abbild ist von 1,59 auf 1,12 GB geschrumpft, seine Fundzahl von 3 kritischen und 45 hohen auf 0 kritische und 6 hohe gefallen). Offen bleiben einzelne Bausteine ohne bereinigte Fassung und die Bausteine, die erst mit einer neuen Hauptversion zu beheben sind; sie werden bei neuen Fassungen der Hersteller erneut angesehen. Der Stand jedes einzelnen Punkts steht in der Tabelle „Einordnung der Befunde“. @@ -208,9 +208,10 @@ Die Tabelle beurteilt jeden Befund der ersten vollständigen Prüfung. Sie wird | Einstellungen zum Absichern der Paketverwaltung (drei Werte) | Arbeitsbereich-Datei | Niedrig | offen | Diese Einstellungen stärken die Lieferkette. Ob sie in der eingesetzten Fassung 9.15 des Paketverwalters wirken, ist noch nicht geprüft | | Fehlende Gesundheitsprüfung im Bauplan des Server-Abbilds | Abbild `api` | Niedrig | bewusst akzeptiert | Die Gesundheitsprüfung liegt in der Compose-Datei, die den Server startet und seine Erreichbarkeit überwacht | | Fehlende Gesundheitsprüfung im Bauplan des Web-Abbilds | Abbild `web` | Niedrig | offen | Weder Bauplan noch Compose-Datei prüfen die Weboberfläche; ein hängender Web-Container würde nicht automatisch erkannt | -| Fehlende Inhaltsrichtlinie und fehlender Schutz gegen Einrahmen (Außenprüfung 9. Oktober 2026) | Weboberfläche, alle Seiten | Mittel | offen | Die Anwendung selbst schickt diese Kopfzeilen bisher nicht mit. Ein vorgeschalteter Proxy kann sie ergänzen; ob er das tut, kann nur der Lauf über die öffentliche Adresse zeigen. Aufgabe des Proxys, beim öffentlichen Lauf erneut prüfen. Fehlen sie dort auch, werden sie in der Anwendung gesetzt. Das tatsächliche Risiko ist gering bis mittel: Es braucht eine fremde Seite, die Benutzer zu Klicks verleitet, oder bereits eingeschleusten Code | -| Weitere fehlende Schutz-Kopfzeilen (Dateityp-Umdeutung, Berechtigungsrichtlinie, drei Cross-Origin-Richtlinien) | Weboberfläche | Niedrig | offen | Reine Vorsorge ohne unmittelbare Gefahr. Teilweise Aufgabe des Proxys (beim öffentlichen Lauf erneut prüfen), sonst kommen sie in die Konfiguration der Weboberfläche | -| Kopfzeile „X-Powered-By“ nennt das Webframework | Weboberfläche | Niedrig | offen | Das Webframework schickt diese Angabe standardmäßig mit. Sie verrät nur den Namen des Frameworks, den Angreifer ohnehin leicht erraten. Wird bei der Härtung der Kopfzeilen mit abgeschaltet | +| Fehlender Schutz gegen Einrahmen (Clickjacking; Außenprüfung 9. Oktober 2026) | Weboberfläche, alle Seiten | Mittel | behoben in der Anwendung, beim öffentlichen Lauf erneut prüfen | Die Anwendung schickt jetzt selbst die Kopfzeilen mit, die das Einrahmen durch fremde Seiten verbieten (X-Frame-Options „SAMEORIGIN“ und die Richtlinie „frame-ancestors“ der Inhaltsrichtlinie); Einrahmen durch Tessera-Seiten selbst bleibt möglich. Eingebettete fremde Seiten (XFrame-Kachel, eigene Module) sind nicht betroffen, die Kopfzeilen schützen nur Tessera. Die Desktop-App lädt die Seite direkt und nicht in einem Rahmen. Ob zusätzlich der Proxy dieselben Kopfzeilen setzt, zeigt der Lauf über die öffentliche Adresse | +| Vollständige Inhaltsrichtlinie (Content Security Policy) | Weboberfläche, alle Seiten | Mittel | offen | Bisher gibt es nur die eine Teilrichtlinie gegen das Einrahmen. Eine vollständige Richtlinie, die festlegt, von wo Skripte und Bilder geladen werden dürfen, braucht eine eigene Prüfung der Inline-Skripte des Webframeworks und der eingebetteten Kacheln, sonst bricht sie Seiten. Das tatsächliche Risiko ist gering bis mittel: Es bräuchte bereits eingeschleusten Code. Wird als eigener Auftrag behandelt | +| Weitere fehlende Schutz-Kopfzeilen (Dateityp-Umdeutung, Berechtigungsrichtlinie, Trennung der Fenster untereinander) | Weboberfläche | Niedrig | behoben in der Anwendung, beim öffentlichen Lauf erneut prüfen | Die Anwendung sendet jetzt die Kopfzeile gegen das Umdeuten von Dateitypen (nosniff), eine Berechtigungsrichtlinie, die Kamera, Mikrofon, Standort, Zahlung und USB sperrt (die Zwischenablage für „Link kopieren“, Vollbild und Benachrichtigungen bleiben erlaubt), die Regel für die Weitergabe der Herkunftsadresse (Referrer-Policy) und die Fensterrichtlinie „same-origin-allow-popups“ (das Anmelde-Fenster zu Nextcloud und andere neue Fenster funktionieren weiter, weil sie ohne Verbindung zum Ursprungsfenster geöffnet werden). Die beiden übrigen Cross-Origin-Richtlinien (Embedder und Resource) bleiben bewusst weg: Sie könnten eingebettete Bilder und Kacheln sperren, ohne hier etwas zu schützen | +| Kopfzeile „X-Powered-By“ nennt das Webframework | Weboberfläche und Server | Niedrig | behoben in der Anwendung, beim öffentlichen Lauf erneut prüfen | Sie ist in der Weboberfläche abgeschaltet; auch der Server (Express) sendet sie nicht mehr. Sie verriet nur den Namen des Frameworks, den Angreifer ohnehin leicht erraten | | Verschlüsselte Verbindung (HTTPS) und deren Absicherung der öffentlichen Adresse | Proxy vor alpha | nicht beurteilt | offen | Der erste Lauf ging über eine unverschlüsselte, interne Verbindung direkt zur Anwendung und konnte das nicht sehen. Aufgabe des Proxys, beim öffentlichen Lauf erneut prüfen | | Drei Informationen der Außenprüfung (Dateityp bei Umleitungen, Zwischenspeicherung) | Weboberfläche | Information | bewusst akzeptiert | Es sind Beschreibungen, keine Schwächen: Anmeldeseiten sind nicht speicherbar, die öffentlichen Programmdateien sind es. Daran ist nichts zu ändern | @@ -240,6 +241,10 @@ Die Beurteilung jeder Meldung steht in der Tabelle „Einordnung der Befunde“. Der neueste Eintrag steht oben. +### 2026-10-09 — Schutz-Kopfzeilen in der Anwendung + +Als Antwort auf den ersten Außenlauf sendet die Weboberfläche jetzt selbst Schutz-Kopfzeilen, damit sie nicht allein vom Proxy abhängen: Schutz gegen das Einrahmen durch fremde Seiten, gegen das Umdeuten von Dateitypen, eine Regel für die Weitergabe der Herkunftsadresse, eine Berechtigungsrichtlinie (Kamera, Mikrofon, Standort, Zahlung und USB gesperrt; die Zwischenablage bleibt erlaubt) und die Fensterrichtlinie „same-origin-allow-popups“. Die Kopfzeile „X-Powered-By“ ist in Weboberfläche und Server abgeschaltet. Vor der Umsetzung wurde geprüft, dass nichts davon eine Funktion bremst: neue Fenster (auch die Nextcloud-Anmeldung) werden ohne Verbindung zum Ursprungsfenster geöffnet, es gibt keinen Austausch zwischen Fenstern, die Desktop-App lädt die Seite direkt, und eingebettete fremde Seiten sind von diesen Kopfzeilen nicht betroffen. Eine vollständige Inhaltsrichtlinie wurde bewusst nicht gesetzt (siehe Tabelle „Einordnung der Befunde“, Zeile „offen“). Ob der Proxy dieselben Kopfzeilen ebenfalls setzt, zeigt der nächste Lauf über die öffentliche Adresse; bis dahin steht der Stand dieser Zeilen auf „behoben in der Anwendung, beim öffentlichen Lauf erneut prüfen“. + ### 2026-10-09 — Eigene Härtung: Echtheits-Code und schlankere Abbilder Zwei Maßnahmen aus der ersten Prüfung wurden umgesetzt. Erstens verlangt die Entschlüsselung gespeicherter Geheimnisse jetzt die volle Länge des Echtheits-Codes; ein gekürzter Code wird abgewiesen, vorher wurde er noch angenommen (der neue Test war vor der Änderung rot). Zweitens enthalten die fertigen Abbilder keine Entwicklungswerkzeuge und keine Paketverwaltung mehr: Das Server-Abbild wird aus einer eigenen Bauphase mit ausschließlich den Betriebsbausteinen zusammengesetzt, und beide Laufzeit-Abbilder entfernen npm, npx, corepack und yarn des Node-Grundabbilds.