Sessions, die halten · Übung
Cookie-Flags mit Absicht
Dein Server beendet seine
session=<id>; HttpOnly; Max-Age=3600; Path=/
Diese Attribute lassen einem Browser die Freiheit, drei Dinge mit dem Cookie zu tun. Er darf es über einfaches HTTP senden, wo jeder auf dem Netzwerkweg es lesen kann. Er darf es mit einem Formular mitsenden, das eine Seite auf einer anderen Website an die Leseliste schickt. Und er darf ein zweites Cookie mit demselben Namen von einem benachbarten Host annehmen und auch dieses senden, und das war der erste Weg aus Lektion 1.
Vier Anweisungen
Ein Cookie-Attribut ist eine Anweisung an den Browser, der das Cookie speichert. Diese Lektion setzt vier davon mit Absicht.
| Attribut | Was es von einem Browser verlangt |
|---|---|
Secure | Sende mich nur über HTTPS. |
HttpOnly | Zeig mich nicht den Skripten einer Seite. |
SameSite=Lax | Wenn eine Seite auf einer anderen Website die Anfrage startet, sende mich nur, wenn der Browser hier eine ganze Seite lädt, etwa weil der Benutzer einem Link folgt, und die Anfrage ein GET ist. Ein Formular, das eine andere Website abschickt, ein Bild, das sie lädt, und eine Anfrage, die ihr Skript stellt, gehen alle ohne mich. |
Path=/ und kein Domain | Ich gehöre genau zu diesem Host und zu jedem Pfad darauf. |
„Eine andere Website“ hat eine genaue Bedeutung. Eine Site ist das Schema plus die registrierbare Domain, der Teil eines Hostnamens, den jemand registriert hat, hier reading.example. Also sind https://notes.reading.example und https://reading.example dieselbe Site. Deshalb tut SameSite nichts gegen den benachbarten Host aus Lektion 1. Das Präfix im nächsten Abschnitt tut es, und Kapitel 8 nimmt sich Anfragen von derselben Site vor.
SameSite hat zwei weitere Werte. Strict würde das Cookie auch zurückhalten, wenn ein Leser von einer anderen Website aus einem Link zur None verlangt gar keinen Schutz.
Eine Seite auf einer anderen Website löst eine Anfrage an die Leseliste aus. Wann hängt ein Browser bei SameSite=Lax das Cookie an?
Das Präfix __Host-
Auch der Name des Cookies kann eine Regel tragen. Ein Browser weigert sich, ein Cookie zu speichern, dessen Name mit __Host- beginnt, es sei denn, das Cookie hat Secure, hat Path=/, hat kein Domain und kam über HTTPS. Ein Cookie, das eine Seite auf notes.reading.example für die ganze Domain setzt, braucht ein Attribut Domain, also kann es nie __Host-session heißen. Kein benachbarter Host kann der Leseliste ein Cookie mit diesem Namen unterschieben.
Der Server erledigt seinen Teil, indem er nur diesen Namen liest. Sobald SESSION_COOKIE "__Host-session" ist, bedeutet ein Cookie namens session für session_id_from nichts mehr, egal wer es untergeschoben hat.
Eine Seite auf notes.reading.example versucht, __Host-session=evil; Domain=reading.example; Secure; Path=/ zu setzen. Was tut ein Browser?
Eine Ausnahme wird beendet
HTTP with Python hat Secure in Lektion 7.1 weggelassen, weil sein Server auf deinem eigenen Computer einfaches HTTP sprach. Diese
pl_sec.py spricht weiterhin einfaches HTTP mit deinem Server auf Secure-Cookie zurück. Manche Browser machen eine Ausnahme für einen Server auf deinem eigenen Computer, die nur für die Entwicklung gedacht ist. Das Werkzeug ist kein Browser: Es baut den Header Cookie von Hand, und ab dieser Lektion sagt es das direkt unter seiner ersten Zeile:
pl_sec sends the __Host-session cookie by hand; a browser would not send it over plain HTTP
Wer gehorcht
Der Server schreibt nur Anweisungen. Ein Test kann prüfen, dass sie geschrieben werden. Ob sie befolgt werden, entscheidet der Browser, und nirgends auf dieser Website prüft ein Browser diese Flags. Ein Programm, das kein Browser ist, kann jeden beliebigen Header Cookie senden, über jede Verbindung. Kapitel 8 schreibt den Teil eines Browsers, der entscheidet, wann ein Cookie gesendet wird.
Dein Test läuft durch: Der Wert von Set-Cookie hat jedes Flag. Was hat der Test gezeigt?
Das Cookie, auf das du hinarbeitest
Wenn max_age standardmäßig ABSOLUTE_LIMIT ist, vergisst ein Browser das Cookie, wenn das absolute Limit des Servers die Session ohnehin beenden würde. Sobald die Flags gesetzt sind, antwortet eine Anmeldung mit:
__Host-session=<id>; HttpOnly; Max-Age=28800; Path=/; SameSite=Lax; Secure
SimpleCookie schreibt die Attribute in alphabetischer Reihenfolge, egal in welcher Reihenfolge du sie setzt, und ein Flag wie Secure ist ein bloßes Wort ohne Wert.
Was gibt das aus?
import http.cookies
cookie = http.cookies.SimpleCookie()
cookie["__Host-session"] = "abc"
cookie["__Host-session"]["path"] = "/"
cookie["__Host-session"]["secure"] = True
cookie["__Host-session"]["samesite"] = "Lax"
print(cookie["__Host-session"].OutputString())
Die Abmeldung braucht keine Änderung. Das Cookie, das eine Session beendet, kommt aus derselben session_cookie("", max_age=0), also hat es denselben Namen und dieselben Flags. Das muss es auch: Ein Browser ignoriert ein __Host--Cookie, dem sie fehlen, und das gilt auch für das, das löscht.
Schreib zuerst den Angriff
Der Angriff ist ein Browser, der tut, was das nackte Cookie erlaubt, also liest der Test die Anweisungen. Melde dich über reading_server.handle(...) als ana an, sichere den Status 200 zu und nimm den Text in response["headers"]["set-cookie"]. Sichere zu, dass er mit __Host-session= beginnt, dass Secure, HttpOnly, SameSite=Lax und Path=/ darin vorkommen und dass Domain nicht vorkommt. Führ python -m pytest -q aus: Es endet mit 1 failed, 10 passed.
Deine Aufgabe
Schreib in
tests/test_attacks.pytest_attack_session_cookie_carries_its_flags()und sieh zu, wie er fehlschlägt.Setz
SESSION_COOKIE = "__Host-session".Setz in
session_cookiesecureaufTrueundsamesiteauf"Lax", auf dieselbe Weise, wiehttponlyundpathgesetzt werden, und ändere den Standardwert vonmax_ageaufABSOLUTE_LIMIT. Setz keine Domain.Nenne in Zeile F4 von
SECURITY.mddeinen Test und setz den Status aufclosed in 4.4.
Führ python -m pytest -q noch einmal aus. Es endet mit 11 passed. Deine zwei früheren Tests dieses Kapitels brauchten keine Änderung, weil steps.cookie_pair den Namen des Cookies nie ausschreibt.
Klicke dann auf Check my work. Die drei Reparaturen dieses Kapitels sind an ihrem Platz. Die nächste Lektion setzt sie zu einer Richtlinie zusammen und kümmert sich um die Sessions, zu denen niemand zurückkommt.
Aufgabe
Schreib einen Test, der die Anweisungen des
Schreib in
tests/test_attacks.pytest_attack_session_cookie_carries_its_flags(). Melde dich überreading_server.handle(...)als ana an, sichere den Status 200 zu und nimmresponse["headers"]["set-cookie"]. Sichere zu, dass dermit __Host-session=beginnt, dass erSecure,HttpOnly,SameSite=LaxundPath=/enthält und dass erDomainnicht enthält. Führpython -m pytest -qaus: Dieser Test muss zuerst fehlschlagen.Setz in
reading_server.pySESSION_COOKIE = "__Host-session".Füge in
session_cookiesecure(aufTruegesetzt) undsamesite(auf"Lax"gesetzt) hinzu und ändere den Standardwert vonmax_ageaufABSOLUTE_LIMIT. Setz keine Domain.Führ
python -m pytest -qaus: Alle 11 Tests laufen durch. Deine zwei früheren Tests dieses Kapitels brauchten keine Änderung, weilsteps.cookie_pairden Namen des Cookies nie ausschreibt.Nenne in Zeile F4 von
SECURITY.mddeinen Test und setz den Status aufclosed in 4.4.
Klicke dann auf Check my work. Es ruft session_cookie auf und zerlegt den Text selbst: Der Name muss __Host-session sein, mit Secure, HttpOnly, SameSite=Lax, Path=/, ohne Domain und mit einem Max-Age von 1 bis ABSOLUTE_LIMIT. Reihenfolge und Groß- und Kleinschreibung der Attribute spielen keine Rolle, und ein Wert, der mit einem session_cookie("", max_age=0) muss denselben Namen und dieselben Flags haben. Über handle(), mit einem Benutzer, den es dir nicht gezeigt hat, meldet es sich an, fragt GET /me mit dem neuen Cookie und mit einem Cookie namens session, das dieselbe ID enthält und null antworten muss, und meldet sich ab, was die reading_server.handle und reading_server.sample_state setzt: einen, dessen Cookie jedes Flag hat, wo dein Test durchlaufen muss; einen, dessen Cookie kein Secure hat, wo er fehlschlagen muss; und einen, der jede Anmeldung ablehnt, wo er fehlschlagen muss. Zuletzt liest es Zeile F4 von SECURITY.md und sucht den Namen deines Tests und einen Status, der mit closed beginnt. Nichts aus deinem