fix(quick-261009-p0m): Schutz-Kopfzeilen in der Weboberfläche, X-Powered-By abgeschaltet

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-10-09 20:29:07 +02:00
parent 2a8a7d4270
commit fceec19ded
8 changed files with 113 additions and 8 deletions
+28
View File
@@ -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 [
+40
View File
@@ -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');
});
});