HTTP-Security-Header einfach erklärt: Welche schützen und welche nur vorhanden sind

Inhalt
Jede Seite, die ein Server ausliefert, kommt mit einem Satz Header – Zeilen, die dem Inhalt vorauslaufen und dem Browser sagen, wie das Folgende zu behandeln ist. Eine Handvoll davon betrifft die Sicherheit.
Diese Header lassen sich leicht prüfen und leicht falsch prüfen. Zu zählen, welche Header vorhanden sind, dauert eine Sekunde und ergibt eine Zahl, die wie ein Ergebnis aussieht. Die Zahl sagt fast nichts, denn ein Header kann vorhanden sein, richtig geschrieben und vollkommen wirkungslos.

Was die Schutz-Header sind
Vier davon tragen die Hauptlast, und jeder beantwortet eine andere Frage.
Content-Security-Policy listet auf, woher die Seite Code laden darf. Alles, was nicht auf der Liste steht, verweigert der Browser – das ist die wichtigste Abwehr gegen eingeschleuste Skripte.
Strict-Transport-Security weist den Browser an, ab jetzt die verschlüsselte Verbindung zu nehmen und nie die offene – auch dann, wenn ein Link, ein Lesezeichen oder eine getippte Adresse etwas anderes sagt.
X-Frame-Options entscheidet, ob die Seite in einem Rahmen auf einer fremden Website eingebettet werden darf, und verhindert damit Klicks auf etwas Unsichtbares.
Referrer-Policy entscheidet, wie viel von der aktuellen Adresse weitergegeben wird, wenn ein Link von der Seite wegführt.
Eine fünfte, Permissions-Policy, schaltet Browserfunktionen ab, welche die Seite nicht braucht – Kamera, Mikrofon, Standort. Dieser Header ist der seltenste der fünf und derjenige, der am wenigsten kaputtmachen kann.
Die Regel, die unsafe-inline aufhebt
Die Content-Security-Policy ist die stärkste der fünf und diejenige, die am häufigsten am eigenen Wert scheitert.
Die Gefahr, für die es sie gibt, ist ein eingeschleustes Skript: etwas, das jemand eingetippt oder ein Angreifer hinterlegt hat, landet in der Seite und läuft, als gehörte es dazu. CSP verhindert das, indem sie die Quellen benennt, aus denen der Browser Code ausführen darf – und ein direkt in die Seite geschriebenes Skript hat keine Quelle, die sich benennen ließe.
Genau das erlaubt 'unsafe-inline' wieder. Das Wort steht nicht ohne Grund im Wert – es sagt, was es tut – und es steht dort, weil eine Seite voller kleiner eingebetteter Skripte ohne diese Erlaubnis aufhört zu funktionieren. Diese Erlaubnis hinzuzufügen ist der schnellste Weg, eine kaputte Seite wieder zum Laufen zu bringen, und gibt genau die Erlaubnis zurück, die zu verweigern der Sinn der Regel war.
Eine Regel mit 'unsafe-inline' blockiert noch ein paar Dinge: von fremden Adressen nachgeladenen Code etwa. Gegen den Angriff, für den es CSP hauptsächlich gibt, tut sie so gut wie nichts.
'unsafe-eval' ist dieselbe Geschichte für einen engeren Fall, und ein default-src von * ist dieselbe Geschichte ganz ohne Vorwand.
Wie lang eine HSTS-Frist sein muss
Strict-Transport-Security hat eine Zahl, an der alles hängt: max-age, in Sekunden.
Dieser Wert sagt, wie lange der Browser die Anweisung behalten soll. Bis zum Ablauf geht keine offene, unverschlüsselte Anfrage mehr an diese Domain – der Browser schreibt sie um, bevor sie den Rechner verlässt. Das ist der ganze Schutz, und er hält genau so lange wie die Erinnerung.
Ein max-age von 300 erinnert fünf Minuten lang. Formal korrekt, in jeder Prüfung vorhanden, die Namen zählt, und nutzlos für einen Besuch am nächsten Tag. Üblich empfohlen ist ein Jahr, geschrieben als 31536000.
Zwei Zusätze zählen. includeSubDomains dehnt die Regel auf jede Unterdomain aus und schließt damit die Lücke, durch die ein vergessenes Testsystem auf derselben Domain unverschlüsselt erreichbar bleibt. Und preload bittet darum, die Domain fest in die Browser einzubauen, sodass der Schutz auch den allerersten Besuch abdeckt – jenen einen Moment, in dem HSTS sonst nicht helfen kann, weil noch nichts gemerkt wurde.
Zu diesem letzten Schritt gehört eine Warnung: Einträge lassen sich schwer entfernen, und bis eine Entfernung in den Browsern ankommt, vergehen Monate. Er passt zu einer Domain, deren Verschlüsselung feststeht, nicht zu einer, die gerade noch geordnet wird.

Wenn zwei Header dasselbe regeln
Das Einbetten in Rahmen ist doppelt geregelt, durch zwei unterschiedlich alte Header, und die Regel darüber, welcher gewinnt, überrascht regelmäßig.
X-Frame-Options ist die ältere, überall verstanden, und bietet DENY oder SAMEORIGIN. CSP hat frame-ancestors, was dieselbe Aufgabe erledigt, aber mit einer Liste von Adressen statt zwei festen Möglichkeiten.
Sind beide vorhanden, gewinnt frame-ancestors, und X-Frame-Options wird vollständig ignoriert. Nicht zusammengeführt, nicht kombiniert – ignoriert. Eine Seite mit X-Frame-Options: DENY und einer CSP mit frame-ancestors * lässt sich von jedem einbetten, und der strengere der beiden Header hat kein Mitspracherecht.
Das ist der Fehler, der sich am besten versteckt, denn beide Header sind vorhanden, beide richtig geschrieben, und eine Prüfung, die Namen zählt, meldet zwei Schutzmaßnahmen, wo eine ist.
Was bei jedem Klick mitgeht
Referrer-Policy ist die leiseste der fünf und diejenige mit den alltäglichsten Folgen.
Führt ein Link von einer Seite weg, teilt der Browser dem Ziel mit, woher der Besuch kam. Wie viel er mitteilt, legt dieser Header fest. unsafe-url gibt die vollständige Adresse samt Pfad weiter, no-referrer gibt nichts weiter, und strict-origin-when-cross-origin gibt innerhalb derselben Website die vollständige Adresse weiter und an fremde nur die Domain.
Im Pfad steckt das Heikle. /de/konto/bestellungen/48120 verrät eine Kundennummer, ein Link zum Zurücksetzen eines Passworts verrät ein Einmalkennwort, und eine Suchseite verrät den Suchbegriff. All das geht an die jeweils verlinkte Website, in einem ganz gewöhnlichen Header, ohne dass irgendetwas schiefgelaufen wäre.
Der letzte der drei Werte ist die vernünftige Voreinstellung, und moderne Browser wenden sie von sich aus an, wenn der Header fehlt. Damit ist ein ausdrücklich gesetztes unsafe-url schlechter als gar kein Header: es ersetzt eine gute Voreinstellung durch eine schlechte Entscheidung.
Was ein einziger Abruf zeigt
All das ist öffentlich. Die Header kommen mit jeder Antwort mit, für jede Adresse, und sie zu lesen kostet eine Anfrage.
Vier Fragen lohnen sich. Gibt es eine CSP, und gibt ihr Wert zurück, was sie genommen hat. Ist die HSTS-Frist lang genug, um zwischen zwei Besuchen zu überdauern. Widersprechen sich die beiden Rahmen-Header, und sagt der gewinnende das Gemeinte. Und gibt die Referrer-Einstellung mehr weiter, als das Ziel braucht.
Der HTTP-Security-Header-Prüfer beantwortet alle vier für eine beliebige Adresse. Er holt die Seite einmal, liest die Header so, wie ein Browser sie liest, verfolgt dabei die Weiterleitungen und beurteilt die Werte, statt die Namen zu zählen.
Das brauchbare Ergebnis ist keine Note. Es ist die kurze Liste der Header, die vorhanden sind und nichts tun – denn genau die werden kein zweites Mal angesehen.
Fragen und Antworten
Wie lässt sich ‘unsafe-inline’ loswerden, ohne jedes eingebettete Skript auszulagern?
Mit Nonces oder Hashes. Eine Nonce ist ein zufälliger Wert, der in der CSP als ‘nonce-…’ steht und als Attribut an jedem erlaubten Skript; der Browser führt dann nur eingebettete Skripte aus, die diesen Wert tragen. Ein Hash (‘sha256-…’) erlaubt ein Skript mit genau diesem Inhalt. Ein eingeschleustes Skript kennt den Wert nicht und passt nicht zum Hash, und genau das stellt den Schutz wieder her, den ‘unsafe-inline’ aufgehoben hat.
Sobald eine Nonce oder ein Hash in der Regel steht, ignorieren aktuelle Browser ein daneben stehendes ‘unsafe-inline’. Es kann deshalb als Rückfall für sehr alte Browser stehen bleiben, ohne den Schutz in den übrigen aufzuweichen.
Zwei Randbedingungen folgen daraus. Eine Nonce muss für jede Antwort neu erzeugt werden; ein Seitencache, der dieselbe HTML-Seite mit derselben Nonce an alle ausliefert, macht sie bekannt und damit wertlos, weshalb sich Hashes mit Caches besser vertragen. Und Ereignis-Attribute wie onclick decken Nonces gar nicht ab, Hashes nur mit dem zusätzlichen Schlüsselwort ‘unsafe-hashes’; sauberer ist der Umzug in Skripte. Bis alles stimmt, zeigt der Header Content-Security-Policy-Report-Only, was eine Regel blockieren würde, ohne es zu blockieren.
Wirkt ein HSTS-Header, der über eine unverschlüsselte Verbindung kommt?
Nein. Browser beachten Strict-Transport-Security nur in Antworten über HTTPS und übergehen den Header in einer unverschlüsselten Antwort, weil ihn dort jeder unterwegs hätte einfügen oder entfernen können. Die Antwort auf http:// sollte deshalb nur auf HTTPS weiterleiten, und der Header gehört in die Antwort, zu der diese Weiterleitung führt.
Was geschieht, wenn Webserver und Anwendung beide eine CSP senden?
Beide gelten. Der Browser führt sie nicht zu einer gemeinsamen Regel zusammen, sondern prüft jede für sich, und eine Quelle wird nur geladen, wenn alle Regeln sie erlauben. Eine zweite, lockere CSP kann eine strenge also nicht aufweichen; eine vergessene strenge Regel, etwa in der Konfiguration des Webservers, blockiert dagegen Dinge, welche die Anwendung in ihrer eigenen Regel ausdrücklich erlaubt.
Bei Strict-Transport-Security ist es anders: Kommt der Header doppelt, wertet der Browser nach RFC 6797 nur den ersten aus. Wer Header an mehreren Stellen setzt, etwa im Webserver, in einem Plugin und in einem CDN, sollte deshalb die tatsächlich ausgelieferte Antwort prüfen und nicht die einzelnen Konfigurationen.
Lässt sich frame-ancestors auch in einem meta-Element setzen?
Nein. Eine CSP im meta-Element versteht frame-ancestors nicht, ebenso wenig report-uri und sandbox, und auch X-Frame-Options wirkt nur als echter Header. Der Schutz vor dem Einbetten braucht deshalb immer eine Einstellung am Server oder im CDN.