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
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 /covers/kindred.txt nennt, in deine Seite. Die Policy aus Lektion 4 erlaubt es, weil die Datei von der eigenen 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
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 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-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 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:
| Header | Wert | Wann |
|---|---|---|
x-content-type-options | nosniff | immer |
referrer-policy | strict-origin-when-cross-origin | immer |
content-security-policy | PAGE_CSP | der Medientyp ist text/html |
cache-control | no-store, ersetzt jeden anderen Wert | der Medientyp ist text/html, oder die Antwort hat einen Header set-cookie |
strict-transport-security | HSTS | https 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 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
Schreib den Test oben und sieh zu, wie er fehlschlägt.
Schreib
hardeninreading_server.py.PAGE_CSPundHSTSwerden oben in der Datei geliefert. Gib die Antwort zurück.Ändere die letzte Zeile von
handle()zureturn harden(response, https=state["https"]).sample_state()hat jetzt einen Schlüssel"https", und er istFalse: Dein Server spricht noch einfaches HTTP. Ein Dienst hinter einem vorgeschalteten Server, derbeendet, würde dasselbe Flag aus seiner Konfiguration setzen. Ändere die letzte Zeile von
closing_error()zureturn harden(response, https=False). Diese Funktion beantwortet eine Anfrage, deren Body nicht gelesen werden kann, etwa ein POST ohneContent-Length. Ihre Antwort entsteht, bevorhandle()läuft, also braucht sie einen eigenen Weg durchharden(). Sie hat keinen State, den sie fragen könnte, also übergibt sieFalse.
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 ruftreading_server.handle()fürGET /aboutauf, für einen Pfad, der nicht existiert, und für eine Anmeldung mit falschem Passwort, und sichert zu, dass jede Antwortx-content-type-optionsgleichnosniffhat. Lies den Header mitresponse["headers"].get("x-content-type-options"), damit ein fehlender Header an der Zusicherung scheitert, stattauszulösen. Sichere zuerst zu, dass GET /aboutmit 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: nosniffundreferrer-policy: strict-origin-when-cross-origin;content-security-policy: PAGE_CSP, wenn der Medientyp voncontent-typetext/htmlist, in beliebiger Groß- und Kleinschreibung und mit oder ohne; cache-control: no-store, das jeden anderenersetzt, wenn der Medientyp text/htmlist oder die Antwort einen Headerset-cookiehat;strict-transport-security: HSTSnur, wennhttpswahr ist. Sonst ändert sie nichts: nicht den Status, nicht den Body und keinen anderen Header.
handle()endet mitreturn harden(response, https=state["https"]), sodass jede Antwort durch sie hindurchgeht, auch Fehler.closing_error()endet mitreturn harden(response, https=False).
Führ python -m pytest -q im
Dann drück Check my work. Die Prüfung ruft harden mit Antworten auf, die sie selbst baut: 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.