Wer was darf, und wie oft · Übung
Jedes Buch hat einen Eigentümer
Lektion 1 hat vorhergesagt, dass ben anas Buch löschen kann. Sieh zu, wie dein Server es tut. Führ im
python pl_sec.py other-reader
pl_sec.py startet deinen Server auf 127.0.0.1 und sendet ihm sechs Anfragen:
pl_sec: other-reader, your server on 127.0.0.1 (a port the system chose)
pl_sec sends the __Host-session cookie by hand; a browser would not send it over plain HTTP
--- ben gets a token of his own ---
ben logs in with a password POST /login -> 200
ben asks for a token, with the cookie POST /tokens -> 200
--- what ben can reach with it ---
ben lists the books GET /books -> 200 {"books": [{"id": 1, "title": "Kindred", "author": "Octavia E. Butler"}, {"id": 2, "title": "Café Society", "author": "Ana Ruiz"}]}
ben deletes book 1, ana's DELETE /books/1 -> 204
ben reads book 1 GET /books/1 -> 404 {"error": "no book has that id"}
a request with no token GET /books -> 200 {"books": [{"id": 2, "title": "Café Society", "author": "Ana Ruiz"}]}
--- result ---
ben deleted ana's book: 204
serve() returned after 6 connections; the server is closed
pl_sec: every thread has stopped
Jeder Berechtigungsnachweis in diesem Lauf ist echt. ben hat sich mit seinem eigenen Passwort angemeldet und sein eigenes Token benutzt. Das ist Befund F8.
Schreib zuerst den Angriff
Der Test wiederholt bens Anfrage. steps.keyed_state() liefert einen Beispielzustand mit dem Testschlüssel darin, und steps.bearer_for(state, "ben", "battery staple") meldet ben an, fragt POST /tokens nach einem Token und gibt den Authorization zurück.
Lass ben zuerst ein eigenes Buch hinzufügen und sichere 201 zu. Das ist die Vorbedingung: Der Test betrifft einen Schreiber, dessen Token funktioniert. Sende dann DELETE /books/1 mit demselben Token und sichere zu, dass der Status 404 ist und dass 1 noch in state["books"] steht.
Führ python -m pytest -q aus. Es endet mit 1 failed, 17 passed, und der Fehlschlag lautet assert 204 == 404.
Ein Eigentümer, den der Client nicht wählen kann
Ein Buch braucht einen Eigentümer, bevor eine Regel nach einem fragen kann. Drei Stellen setzen ihn.
Gib in sample_state() beiden Büchern "owner": "ana".
create_book bekommt einen dritten owner, und speichert ihn im neuen Buch. route übergibt claims["user"]. Dieser Name kommt aus einem Token, das dein Server signiert hat, also kann der Client ihn nicht wählen. Ein Body, der "owner": "ana" sagt, ändert nichts, weil create_book weiterhin nur title und author aus dem Body liest.
replace_book behält den Eigentümer, den das gespeicherte Buch schon hat. Kapitel 7 geht einen Schritt weiter und lehnt einen Body ab, der Felder trägt, nach denen niemand gefragt hat.
Eine Regel, zwei Fragen
may_view(user, book) entscheidet über jeden Lesezugriff und may_change(user, book) über jeden Schreibzugriff. Heute sind beide derselbe Satz: Der Eigentümer des Buchs ist dieser Benutzer. Es sind zwei
Beide geben False zurück, wenn user None ist oder book None ist, bevor sie irgendetwas vergleichen. Das schließt die Falle None == None aus Lektion 1.
Ablehnen, sofern keine Regel erlaubt
Schreib jetzt item_refusal neu. Die mitgelieferte Version listet auf, was abgelehnt wird, und erlaubt am Ende den Rest. Dreh das um:
Ohne Claims gib
unauthorized("Bearer")zurück, für jede. GETundHEADgebenNonezurück, wennmay_viewes erlaubt.PUTundDELETEgeben die 403 zurück, wenn das Konto überhaupt nicht schreiben darf, undNone, wennmay_changees erlaubt.Jede andere Methode gibt
not_allowed("GET, HEAD, PUT, DELETE")zurück, aber nur für ein Buch, das der Aufrufer sehen darf.Die letzte Zeile gibt
json_error(404, "no book has that id")zurück.
return None steht jetzt nur noch innerhalb einer Regel. Alles, was keine Regel erlaubt hat, und jeder Fall, an den niemand gedacht hat, endet in der letzten Zeile. Diese Form heißt standardmäßig verweigern (deny by default).
Drei Ablehnungen stehen jetzt nebeneinander. 403 ist für ein Konto, das überhaupt nicht schreiben darf, wie in
Nächstes Jahr lässt jemand PATCH bis zu item_refusal durch und schreibt keine Regel dafür. Was bekommt ein PATCH von ben auf anas Buch mit der Funktion, die du geschrieben hast?
Lesezugriffe sind jetzt persönlich
collection_refusal bekommt dieselbe erste Zeile: keine Claims, kein Zugriff, für jede Methode. /books zu lesen braucht jetzt ein Token, und die Listenschleife behält nur die Bücher, die may_view erlaubt. Das ist eine bewusste Änderung gegenüber HTTP with Python, wo die
Eine Antwort, die davon abhängt, wer gefragt hat, darf nicht aus einem Cache an jemand anderen ausgeliefert werden. Das Projekt setzt bei beiden Lesezugriffen auf Bücher bereits Cache-Control: private, no-cache. private bedeutet, dass ein gemeinsamer Cache die Antwort nicht speichern darf. no-cache bedeutet, was es in HTTP with Python bedeutet hat: Du darfst sie behalten, aber frag mich, bevor du sie benutzt.
Das zweite Wort ist nötig, weil der eigene Cache eines Browsers eine gespeicherte Antwort über ihre max-age könnte eine Liste, die für einen Leser gespeichert wurde, eine Minute lang für eine Anfrage angezeigt werden, die das Token eines anderen Lesers trägt. Mit no-cache fragt der Browser mit If-None-Match erneut, und der Server, der das Token sieht, entscheidet jedes Mal: 304, wenn die eigene Liste des Aufrufers unverändert ist, sonst 200 mit der Liste des Aufrufers.
Ob ein Cache gehorcht, ist Sache des Caches. Der Teil deines Servers ist, es zu sagen. GET /about bleibt öffentlich und behält max-age=60.
ben fragt nach /books/1, das ana gehört, und nach /books/4100, das nicht existiert. Warum sollten die beiden Antworten gleich sein?
Was verlangt Cache-Control: private, no-cache?
Deine Aufgabe
Schreib
test_attack_another_reader_cannot_delete_a_book()und sieh zu, wie er fehlschlägt.Gib beiden Beispielbüchern einen Eigentümer, gib
create_bookseinen Parameterownerund behalte inreplace_bookden gespeicherten Eigentümer.Schreib
may_viewundmay_change.Schreib
collection_refusalunditem_refusalso um, dassNonenur unter einer Regel zurückgegeben wird.Behalte in der Listenschleife in
routenur die Bücher, diemay_view(claims["user"], book)erlaubt.Fülle die Zeile F8 von
SECURITY.mdaus, nenne darin deinen Test und setz ihren Status aufclosed in 6.2.
Führ python -m pytest -q noch einmal aus: 18 passed. Führ dann noch einmal python pl_sec.py other-reader aus. bens Liste ist leer, sein Löschen und sein Lesen von Buch 1 bekommen beide 404, die Anfrage ohne Token bekommt 401, und der Lauf endet mit ben was refused: 404, and book 1 is still there.
Eines bleibt sichtbar. Buch-IDs zählen weiterhin für alle aufwärts, also zeigt die ID, die ein neues Buch bekommt, wie viele Bücher es insgesamt gibt. Und die Anmeldung, die ben sein Token gegeben hat, kann immer noch so oft versucht werden, wie jemand will. Das sind die nächsten zwei Lektionen.
Aufgabe
Gib jedem Buch einen Eigentümer und sorg dafür, dass die Zugriffsentscheidung ablehnt, sofern keine Regel erlaubt.
Schreib in
tests/test_attacks.pytest_attack_another_reader_cannot_delete_a_book(). Hol dir mitstate = steps.keyed_state()bearer = steps.bearer_for(state, "ben", "battery staple"). SendePOST /booksmit einem gültigen Buch und bens Token im HeaderAuthorizationund sichere den Status 201 zu. SendeDELETE /books/1mit bens Token. Sichere den Status 404 zu und dass1noch instate["books"]steht. Führpython -m pytest -qaus: Dieser Test muss zuerst fehlschlagen.Gib in
reading_server.pyinsample_state()beiden Büchern"owner": "ana".Gib
create_bookeinen dritten, owner, speichere ihn im Buch und übergibclaims["user"]dort, woroutees aufruft. Behalte inreplace_bookden Eigentümer, den das gespeicherte Buch schon hat.Schreib
may_view(user, book)undmay_change(user, book):False, wennuserNoneist oderbookNoneist; andernfalls, obbook.get("owner")gleichuserist.Schreib
collection_refusal(method, claims)neu: Keine Claims bedeutenunauthorized("Bearer")für jede; GETundHEADsind erlaubt;POSTist erlaubt, wennclaims["can_write"], und sonst eine 403; alles andere istnot_allowed("GET, HEAD, POST").Schreib
item_refusal(method, claims, book)so um, dass esNonenur zurückgibt, wenn eine Regel erlaubt: Keine Claims bedeuten 401;GETundHEADbrauchenmay_view;PUTundDELETEantworten mit 403, wenn das Konto nicht schreiben darf, und brauchenmay_change; jede andere Methode auf einem Buch, das der Aufrufer sehen darf, istnot_allowed("GET, HEAD, PUT, DELETE"); die letzte Zeile gibtjson_error(404, "no book has that id")zurück.Behalte in der Listenschleife in
routenur die Bücher, diemay_view(claims["user"], book)erlaubt. Lass die zwei Zeilen mitPRIVATE_CACHE, wie sie sind.Fülle die Zeile F8 von
SECURITY.mdaus, nenne deinen Test in ihrer Zelle Test und setz den Status aufclosed in 6.2.
Führ python -m pytest -q aus, bis es mit 18 passed endet, und klicke dann auf Check my work. Es ruft may_view, may_change, collection_refusal und item_refusal mit eigenen Benutzern und Büchern auf. Es sendet Anfragen über reading_server.handle(...) mit Tokens, die es selbst signiert: die Cache-Control, das Buch eines anderen Lesers mit jeder Methode, ein Konto, das nur lesen darf, und Bodys, die einen Eigentümer nennen. Es führt deinen Test dreimal aus, gegen Server, die es an die Stelle von reading_server.handle und reading_server.sample_state setzt: einen korrekten, wo dein Test durchlaufen muss; einen, dessen Schreibzugriffe nicht gegen den Eigentümer geprüft werden, wo er fehlschlagen muss; und einen, der jede Anfrage ablehnt, wo er ebenfalls fehlschlagen muss. Es liest die Zeile F8 von SECURITY.md; die Formulierung einer Zelle wird nicht bewertet. Nichts aus deinem