~/blog/legacy-municipal-cms-security-headersPOSTED
von Shady Nathan Tawfik · 2. Mai 2026 · 3 min

Ein Legacy-Kommunal-CMS mit perfekter Transportverschlüsselung und ohne Clickjacking-Schutz

HSTS war auf einem deutschen Kommunalportal einwandfrei. Ein fehlender Header erlaubte Framing weiterhin, und die CSP erlaubte Skript-Injektion.

Interaktiv[blog-2.0]

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.

TL;DR: HSTS war auf einem deutschen Kommunalportal einwandfrei. Ein fehlender Header erlaubte Framing weiterhin, und die CSP erlaubte Skript-Injektion.

Befunde

SchweregradBefundNachweis
HOCHContent Security Policy erlaubt Inline- und dynamische Skriptescript-src enthielt die Schlüsselwörter für Inline und dynamische Auswertung
MITTELKein Frameschutz auf VerwaltungspfadenClickjacking-Fläche in jeder angemeldeten Ansicht
MITTELStyle-Policy ebenfalls zu permissivInline-Styles erlaubt, Style-Grenze geschwächt
NIEDRIGVeralteter Frame-Options-Header nur auf der Login-SeiteInkonsistente Policy zwischen Login und Anwendung
NIEDRIGServer-Banner nennt die Webserver-VersionVersionsinformation in jeder Antwort
INFOStrict Transport Security korrekt konfiguriertLanges max-age mit Subdomain und Preload-Absicht

Der Angriffspfad

Third party pageInvisible iframeAdmin view without frame policyUser clicks decoyFramed button receives the clickAction executed in victim session

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

~/code/bash bash
# 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

~/code/bash bash
# 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

~/code/nginx nginx
# 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:

Content Security Policy erlaubt Inline- und dynamische Skripte
Kein Frameschutz auf Verwaltungspfaden
Style-Policy ebenfalls zu permissiv

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.

Nur zu Bildungszwecken. Alle reproduzierbaren Befehle zielen auf meine eigene Lab-Umgebung (localhost), nie auf ein laufendes System. © 2026