TL;DR: Ein Legacy-Kommunal-CMS antwortete mit starken Headern bis auf zwei Lücken: unsafe-inline in der CSP und fehlender Frameschutz. Ich zeige, wie ich beide verifiziert habe, warum sie zusammenhängen und wie man sie repariert, ohne das Legacy-Backend zu brechen.
Ausgangslage
Ich habe das Ziel als Black-Box geprüft: keine Zugangsdaten, nur die öffentlich sichtbare Fläche. Das ist, was ich sah, bevor ich etwas angefasst habe.
Ein Portal eines deutschen Regionalgerichts mit einem proprietären kommunalen CMS aus der Mitte der 2010er, gehostet auf Apache hinter einem CDN. Geprüft wurden Response-Header, Frame-Policy, TLS-Konfiguration und die JavaScript-Auslieferung.
Ich habe alle identifizierenden Informationen entfernt. Das Ziel wird ausschließlich nach Branche und Technologieklasse beschrieben, damit das Muster übertragbar bleibt.
Das Muster
Das Muster, das ich erkannt habe — und das ich in ähnlichen Landschaften immer wieder finde:
Eine strenge Transportrichtlinie und eine fehlende Frame-Policy sind zwei unabhängige Probleme. HSTS sagt dem Browser nur, Klartext für eine Domain abzulehnen. Der Frameschutz verhindert dagegen, dass eine andere Origin deine Oberfläche in einem unsichtbaren Container rendert und deine Klicks weiterleitet.
Die zweite Hälfte des Fehlers ist die Content Security Policy. Enthält die Script-Quellenliste die Schlüsselwörter für Inline-Skripte und dynamische Auswertung, ist die Policy keine Grenze mehr, sondern ein Kommentar. Jeder Injektionspunkt, der einen HTML-Sink erreicht, führt dann mit vollen Seitenrechten aus.
Kommunale Umgebungen zeigen besonders deutlich, warum das zählt. Eine single eingebettete Verwaltungsseite auf einer Drittseite kann eine legitim wirkende Freigabe auslösen, während der Nutzer glaubt, woanders zu sein. Der Audit erfordert keine Schwachstelle im CMS selbst, genau darum überlebt das Muster jahrelang.
Befunde
| Schweregrad | Befund | Nachweis |
|---|---|---|
| HOCH | Content Security Policy erlaubt Inline- und dynamische Skripte | script-src enthielt die Schlüsselwörter für Inline und dynamische Auswertung |
| MITTEL | Kein Frameschutz auf Verwaltungspfaden | Clickjacking-Fläche in jeder angemeldeten Ansicht |
| MITTEL | Style-Policy ebenfalls zu permissiv | Inline-Styles erlaubt, Style-Grenze geschwächt |
| NIEDRIG | Veralteter Frame-Options-Header nur auf der Login-Seite | Inkonsistente Policy zwischen Login und Anwendung |
| NIEDRIG | Server-Banner nennt die Webserver-Version | Versionsinformation in jeder Antwort |
| INFO | Strict Transport Security korrekt konfiguriert | Langes max-age mit Subdomain und Preload-Absicht |
Der Angriffspfad
Reproduktion im Labor
Jeder Befehl unten zielt auf einen Lab-Container, den ich selbst kontrolliere. Nichts davon ist auf ein laufendes System gerichtet.
Rahmen Sie die Anwendung selbst ein und sehen Sie, ob der Browser es verbietet
# Ziel: eine Seite in IHRER eigenen lokalen Lab-Umgebung
$ cat > /tmp/lab.html <<'EOF'
<iframe src="http://localhost:8080/admin/" width="600" height="400"></iframe>
EOF
$ python3 -m http.server 9000 --directory /tmp
# http://localhost:9000/lab.html öffnen
# rendert der Frame, ist die Anwendung einbettbar
Erkennung in eigener Infrastruktur
Header-Vorhandensein und Werte
# read-only header probe against a host you own or are authorised to test
$ curl -sI https://example.org/ | grep -iE \
'strict-transport|content-security-policy|x-frame-options|x-content-type|referrer-policy|permissions-policy'
# presence check, exit code 0 when every header is present
$ for h in strict-transport-security content-security-policy x-frame-options \
x-content-type-options referrer-policy; do
curl -sI https://example.org/ | grep -qi "^$h" && echo "ok $h" || echo "MISS $h"
done
Behebung
Vollständige Basis ausliefern und die CSP bedeutsam machen
# nginx: send the full baseline and stop proxy layers from stripping it
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
# a CSP without unsafe-inline and without unsafe-eval, no wildcard CDNs
add_header Content-Security-Policy \
"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; \
frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
Sobald ein Angreifer Zugriff hat, kann er folgende Aktionen durchführen:
Fazit — was ich daraus mitnehme
- Transportverschlüsselung und Frameschutz sind getrennte Kontrollen und getrennt zu prüfen.
- Eine CSP, die Inline-Skripte erlaubt, ist Dokumentation und keine Abwehr.
- Frameschutz gehört auf jeden Pfad, auch auf angemeldete Verwaltungsansichten.
- Versionsbanner kosten nichts zu entfernen und ersparen dem Angreifer einen kompletten Recon-Schritt.