0%

HTTPS, Zertifikate und Sicherheits-Header · Übung

Ein harden() für jede Antwort

Du kannst jetzt eine Browser-Anweisung lesen, und dein Dienst sendet keine. Diese Lektion fügt vier hinzu, über eine , zu jeder Antwort, die dein Code erzeugt. Wie in jedem Kapitel kommt der Angriff zuerst, als Test.

Füg diesen Test in tests/test_attacks.py ein, unter dem Kommentar zu Kapitel 10 am Ende der Datei:

def test_attack_every_response_says_nosniff():
    """Every response, an error included, must tell a browser to believe Content-Type."""
    state = reading_server.sample_state()
    about = reading_server.handle(helpers.make_request("GET", "/about"), state)
    assert about["status"] == 200
    assert about["headers"].get("x-content-type-options") == "nosniff"
    missing = reading_server.handle(helpers.make_request("GET", "/nowhere"), state)
    assert missing["status"] == 404
    assert missing["headers"].get("x-content-type-options") == "nosniff"
    headers = {"Authorization": helpers.basic("ana", "a wrong guess"), "Origin": steps.OWN}
    refused = reading_server.handle(helpers.make_request("POST", "/login", headers=headers), state)
    assert refused["status"] == 401
    assert refused["headers"].get("x-content-type-options") == "nosniff"

Er fragt eine öffentliche Route an, einen Pfad, der nicht existiert, und eine Anmeldung, die fehlschlägt. Die erste Zusicherung ist seine Vorbedingung: Die öffentliche Route antwortet mit 200. Führ python -m pytest -q aus und sieh zu, wie er bei der zweiten Zusicherung fehlschlägt. Der Rest dieser Lektion bringt ihn zum Bestehen.

Glaub dem Etikett

Ein Browser rät manchmal anhand der Bytes, was eine Antwort ist, egal was Content-Type sagt. Dieses Raten heißt Sniffing. Dein Dienst gibt ihm etwas zum Raten. Ein Cover wird als text/plain ausgeliefert, und sein Text stammt von dem, der das Cover geliefert hat. Angenommen, ein Cover enthält -Text, und ein übersehenes Escapen lässt ein Script-Tag, das /covers/kindred.txt nennt, in deine Seite. Die Policy aus Lektion 4 erlaubt es, weil die Datei von der eigenen der Seite kommt. Ohne nosniff führt ein Browser es womöglich aus.

X-Content-Type-Options: nosniff sagt dem Browser, dass er dem Etikett glauben soll. Damit weigert sich ein Browser, ein Skript auszuführen, dessen angegebener kein Skripttyp ist. Der Header kostet nichts und passt zu jeder Antwort, also bekommt ihn jede Antwort.

Ein Cover wird als text/plain ausgeliefert und enthält JavaScript-Text. Eine Seite nennt es in einem Script-Tag. Die Antwort sagt nosniff. Was macht ein Browser?

Bleib bei HTTPS

Strict-Transport-Security, kurz HSTS, ist ein Versprechen über die Zukunft: Dieser Host spricht HTTPS, für mindestens max-age Sekunden. Ein Browser merkt sich das. Von da an macht er jeden http://-Link zu diesem Host zu https://, bevor er etwas sendet. Und wenn eine Zertifikatsprüfung für diesen Host fehlschlägt, soll der Browser keinen Weg anbieten, sich am Fehler vorbeizuklicken. Das macht die Ablehnungen, die du in Lektion 3 gesehen hast, endgültig.

Der Header wird nur über HTTPS gesendet. Über einfaches könnte ihn jeder auf dem Weg hinzufügen oder entfernen, also ignoriert ein Browser ihn dort, und der Standard (RFC 6797) sagt, dass man ihn dort nicht senden soll. Deshalb nimmt harden() einen https.

Zwei Grenzen. Der erste Besuch ist nicht geschützt, weil der Browser noch nichts zum Merken hat. Für diesen Fall bringen Browser Preload- von Hosts mit, und das Wort preload in einem HSTS-Header gehört zu diesem Verfahren. Und der gelieferte , HSTS = "max-age=31536000", ist ein Jahr ohne includeSubDomains. Dieses Wort würde jeden Host unter reading.example binden, und notes.reading.example wird von anderen Leuten betrieben.

Warum fügt harden() strict-transport-security nur hinzu, wenn https wahr ist?

Der Browser eines Lesers hat die Site noch nie kontaktiert. Welche Anfrage schützt HSTS nicht?

Sag anderen Sites weniger

Wenn ein Leser einem Link weg von deiner Seite folgt, sagt der Browser der nächsten Site in einem Header Referer, woher der Leser kam. Dein Dienst protokolliert Pfade und nimmt einen Parameter next, also sagt er dem Browser, dass er seine nicht weitergeben soll: Referrer-Policy: strict-origin-when-cross-origin. Eine andere Site erfährt dann die Origin und nie den Pfad. Aktuelle Browser verhalten sich standardmäßig so. Der Header legt das Verhalten für ältere Browser fest.

Warum nicht no-referrer, das noch weniger sagt? Weil dieser Wert auch den Header Origin ändert. Damit sendet ein Browser Origin: null mit jedem Formular, das deine eigenen Seiten absenden: dem Löschformular und den Formularen für Anmeldung, Abmeldung und Seitengröße. Die Prüfung, die du in Kapitel 8 geschrieben hast, lehnt null ab, und das soll sie auch. Der strengere Wert würde also deine eigenen Seiten aussperren.

Jemand ändert die Policy auf no-referrer, um noch weniger zu sagen. Was funktioniert nicht mehr?

Eine Funktion, entschieden anhand der Antwort

harden(response, *, https) bekommt eine Antwort und eine Tatsache über die Verbindung. Sie hat keine Anfrage und keinen State, also wird jede Regel anhand der Antwort selbst entschieden:

HeaderWertWann
x-content-type-optionsnosniffimmer
referrer-policystrict-origin-when-cross-originimmer
content-security-policyPAGE_CSPder Medientyp ist text/html
cache-controlno-store, ersetzt jeden anderen Wertder Medientyp ist text/html, oder die Antwort hat einen Header set-cookie
strict-transport-securityHSTShttps ist wahr

Der Medientyp ist der Teil von content-type vor einem eventuellen ;, gestrippt und in Kleinbuchstaben. Eine Antwort ohne content-type hat keinen.

Die Zeile no-store ist ein Sicherheitsnetz unter einer Regel, die du schon hast. Seit Kapitel 4 wird eine Antwort, die ein setzt, nie gespeichert, und die Seite zeigt die Bücher eines Lesers und sein Formular-Token. harden() lässt jedes andere cache-control und etag so, wie die Route sie erstellt hat: GET /about behält max-age=60, und ein Lesezugriff mit Token behält private, no-cache.

Deine Aufgabe

  1. Schreib den Test oben und sieh zu, wie er fehlschlägt.

  2. Schreib harden in reading_server.py. PAGE_CSP und HSTS werden oben in der Datei geliefert. Gib die Antwort zurück.

  3. Ändere die letzte Zeile von handle() zu return harden(response, https=state["https"]). sample_state() hat jetzt einen Schlüssel "https", und er ist False: Dein Server spricht noch einfaches HTTP. Ein Dienst hinter einem vorgeschalteten Server, der beendet, würde dasselbe Flag aus seiner Konfiguration setzen.

  4. Ändere die letzte Zeile von closing_error() zu return harden(response, https=False). Diese Funktion beantwortet eine Anfrage, deren Body nicht gelesen werden kann, etwa ein POST ohne Content-Length. Ihre Antwort entsteht, bevor handle() läuft, also braucht sie einen eigenen Weg durch harden(). Sie hat keinen State, den sie fragen könnte, also übergibt sie False.

python -m pytest -q

Alle Tests bestehen, auch der neue. Dann drück Check my work. Jede Antwort, die dein Code erzeugt, trägt jetzt ihre Anweisungen, und eine davon, HSTS, wurde noch nie gesendet, weil der Server noch kein HTTPS spricht. Das Projekt ändert das.

Aufgabe

Füg die Browser-Anweisungen zu jeder Antwort hinzu, in reading_server.py, und beweise eine davon mit einem Test.

In tests/test_attacks.py:

  • Schreib test_attack_every_response_says_nosniff. Der Test ruft reading_server.handle() für GET /about auf, für einen Pfad, der nicht existiert, und für eine Anmeldung mit falschem Passwort, und sichert zu, dass jede Antwort x-content-type-options gleich nosniff hat. Lies den Header mit response["headers"].get("x-content-type-options"), damit ein fehlender Header an der Zusicherung scheitert, statt auszulösen. Sichere zuerst zu, dass GET /about mit 200 antwortet.

In reading_server.py:

  • harden(response, *, https) ändert die Header der Antwort und gibt die Antwort zurück:

    • immer x-content-type-options: nosniff und referrer-policy: strict-origin-when-cross-origin;

    • content-security-policy: PAGE_CSP, wenn der Medientyp von content-type text/html ist, in beliebiger Groß- und Kleinschreibung und mit oder ohne ;

    • cache-control: no-store, das jeden anderen ersetzt, wenn der Medientyp text/html ist oder die Antwort einen Header set-cookie hat;

    • strict-transport-security: HSTS nur, wenn https wahr ist. Sonst ändert sie nichts: nicht den Status, nicht den Body und keinen anderen Header.

  • handle() endet mit return harden(response, https=state["https"]), sodass jede Antwort durch sie hindurchgeht, auch Fehler.

  • closing_error() endet mit return harden(response, https=False).

Führ python -m pytest -q im aus: Jeder Test besteht.

Dann drück Check my work. Die Prüfung ruft harden mit Antworten auf, die sie selbst baut: , eine Seite, Antworten, die setzen, und eine Antwort ohne Header. Sie ruft handle mit Benutzern, Büchern und einer Uhr auf, die du nicht gesehen hast, mit state["https"] sowohl False als auch True, und sendet das eigene Löschformular der Seite so, wie es ein Browser täte. Sie sendet ein POST ohne Content-Length im Speicher durch http_adapter. Sie führt deinen Test gegen einen Server aus, der den Header überall sendet, und dort muss er bestehen. Gegen einen Server, dessen Fehlerantworten ihn nicht haben, muss er fehlschlagen. Keine Prüfung öffnet eine Verbindung. Nichts aus deinem Terminal wird bewertet.