Eingaben, die zu Code werden · Abschlussprojekt
Abschlussprojekt: Projekt: Nichts kommt ungeprüft durch
Dieses Kapitel hat eine Regel in zwei Hälften. Prüfe einen Body auf dem Weg hinein, mit einer Allowlist und Grenzen. Schütze jede Stelle, an der Text hinausgeht in etwas, das geparst wird: einen Header, eine <b>Dune</b> ist ein gültiger Titel, und er muss in der Seite trotzdem escapet werden.
Der Validator und die vier Schutzmaßnahmen sind geschrieben. Zwei der fünf haben einen Angriffstest. Dieses Projekt schreibt die anderen drei Tests, fügt eine Schutzmaßnahme für alles hinzu, was handle() verlässt, und schließt zwei Befunde.
Drei weitere Angriffstests
Jeder Test führt einen Angriff aus und stellt sicher, dass er abgelehnt wird, wie in den Lektionen 1 und 4. Jeder stellt außerdem sicher, dass eine ehrliche Anfrage funktioniert, damit er nicht gegen einen Server bestehen kann, auf dem nichts funktioniert.
test_attack_next_cannot_leave_the_site. Lass eine next laufen: %2F%2Fother.example%2Fwelcome, %2F%5Cother.example und https:%2F%2Fother.example%2F. Melde für jeden davon ana mit einem frischen Zustand an und stelle sicher, dass der Status 303 ist und dass location genau / ist. Die 303 ist die ehrliche Hälfte: Das Login hat funktioniert.
test_attack_cover_name_cannot_leave_the_folder. Dieser Test bringt seinen eigenen Ordner mit. Pytest gibt jeder Testfunktion, die einen tmp_path hat, einen frischen temporären Ordner als Path. Fang so an:
def test_attack_cover_name_cannot_leave_the_folder(tmp_path):
"""A cover name with an encoded ../ in it must not reach a file outside the covers folder."""
covers = tmp_path / "covers"
covers.mkdir()
(covers / "kindred.txt").write_text("a cover", encoding="utf-8")
(tmp_path / "secret.txt").write_text("not a cover", encoding="utf-8")
state = reading_server.sample_state()
state["covers_dir"] = covers
Stelle dann 200 für GET /covers/kindred.txt sicher. Stelle 404 für GET /covers/..%2fsecret.txt sicher, und dass b"not a cover" nicht in diesem Body steht.
test_attack_title_cannot_become_markup. Lege mit steps.keyed_state() und einem Token für ana ein Buch mit dem Titel <b>Dune</b> an und stelle 201 sicher. Hol dir ihr steps.login_cookie(state, "ana", "correct horse"), sende damit GET / in einem Cookie-Header und stelle 200 sicher. Dekodiere den Body und stelle sicher, dass "<b>" nicht im Text steht und "<b>Dune</b>" schon.
Alle drei bestehen, sobald du sie schreibst, weil die Schutzmaßnahmen schon da sind. Das beweist wenig. Sieh jeden Test einmal fehlschlagen: Schalte seine Schutzmaßnahme von Hand ab, führe python -m pytest -q aus und schalte sie wieder ein.
Setze
return valueals erste Zeile insafe_redirect_target.Setze
return pathlib.Path(folder) / nameals erste Zeile insafe_cover_path.Ändere in
render_pagehtml.escape(book["title"])zubook["title"].
Jedes Mal endet der Lauf mit einem Fehlschlag, und es ist der Test für diese Schutzmaßnahme.
Check my work macht dasselbe gründlicher. Es führt jeden deiner fünf Tests gegen einen eigenen Server aus, der alle Schutzmaßnahmen hat, wo der Test bestehen muss. Dann führt es den Test gegen einen Server aus, dem genau diese eine Schutzmaßnahme fehlt, wo der Test fehlschlagen muss.
Dein Test für die Cover-Route besteht. Check my work setzt einen Server ein, dessen Cover-Route nichts prüft, und dein Test besteht immer noch. Was sagt dir das?
Eine Schutzmaßnahme für alles, was hinausgeht
redirect() schützt einen Header. Dein Server schreibt weitere, etwa etag, allow und set-cookie, und nächsten Monat baut ein neuer Zweig vielleicht einen aus etwas, das eine Anfrage gesendet hat. Jede Antwort läuft durch handle(), also deckt eine Schleife dort sie alle ab:
try:
for name in response["headers"]:
values = response["headers"][name]
if isinstance(values, str):
values = [values]
for value in values:
header_value(value)
except (ValueError, TypeError):
Ein Header-Wert ist ein Text oder eine http_adapter schreibt jedes Element einer Liste als eigene Zeile. Also macht die Schleife aus einem einzelnen Text eine Liste mit einem Element und prüft jedes Element. Das except nennt auch 0, lässt header_value diesen Fehler auslösen, und dieser Fehler sollte in derselben festen 500 enden.
Den except-Zweig schreibst du. Er ersetzt die Antwort durch die feste 500 aus Kapitel 6: json_error(500, ...) mit derselben Meldung, cache-control auf no-store gesetzt und ein leerer Body, wenn die HEAD ist. Setze den ganzen Block hinter das try, das route() aufruft, und vor die Log-Zeile, damit das Log den Status zeigt, der gesendet wurde.
Das ist „fail closed“, die Regel aus Kapitel 5, angewendet auf die Ausgabe. Ein Header-Fehler in irgendeinem Zweig wird zu einem Fehler, den du im Log sehen kannst, und nicht zu einer Antwort mit einer Zeile, die jemand anderes geschrieben hat. Die Schleife prüft nur Header. Einen Body prüft der Code, der ihn schreibt, so wie render_page es tut.
Meilensteine
Die drei Tests, jeder einmal fehlschlagend gesehen, mit abgeschalteter Schutzmaßnahme. Mit allen Schutzmaßnahmen zurück endet
python -m pytest -qmit26 passed.Die Schleife in
handle(). Die Tests enden weiterhin mit26 passed.Das ganze Szenario:
python pl_sec.py injection
Lies alle vier Teile. Das Ergebnis lautet:
--- result ---
0 of 4 attacks worked
Die Befunde. Fülle in
SECURITY.mddie Zeilen F10 und F11 mit deinen eigenen Worten aus. F10 ist der Body, der den Eigentümer eines Buchs gesetzt hat, und sein Test isttest_attack_body_cannot_set_the_owner. F11 ist Anfragetext, der in einem Header, einer Weiterleitung, einem Pfad und einer Seite als Anweisungen gelesen wurde, und er nennt die anderen vier Tests. Setze beide aufclosed in 7.8.
Was noch offen ist
Sieh dir das Löschformular noch einmal an. POST /books/delete wird allein durch das Session-Cookie autorisiert, und nichts in der Anfrage zeigt, dass sie von deiner Seite kam. Kapitel 8 baut ein kleines Modell eines Browsers und sendet damit dieses Formular von der Seite eines anderen aus.
Aufgabe
Schreib die letzten drei Angriffstests des Kapitels, schütze jeden Header, der handle() verlässt, und schließe zwei Befunde.
Schreib in
tests/test_attacks.pyunter dem Kommentar für Kapitel 7 drei Tests. Erreiche den Server überreading_server.handle(...).test_attack_next_cannot_leave_the_site(): Melde für jedesnextaus%2F%2Fother.example%2Fwelcome,%2F%5Cother.exampleundhttps:%2F%2Fother.example%2Fana mitPOST /login?next=und dieseman und stelle sicher, dass der Status 303 ist und dass response["headers"]["location"] == "/"gilt.test_attack_cover_name_cannot_leave_the_folder(tmp_path): Legetmp_path / "covers"mit einer Dateikindred.txtan, schreibsecret.txtneben den Ordner, setzestate["covers_dir"]auf den Ordner, stelle 200 fürGET /covers/kindred.txtsicher und stelle 404 fürGET /covers/..%2fsecret.txtsicher, und dass der Text des Geheimnisses nicht im Body steht.test_attack_title_cannot_become_markup(): Lege mitsteps.keyed_state()undsteps.bearer_for(state, "ana", "correct horse")ein Buch mit dem Titel<b>Dune</b>an und stelle 201 sicher; holGET /mit demvon steps.login_cookie(state, "ana", "correct horse")ab und stelle 200 sicher; stelle sicher, dass"<b>"nicht im Text der Seite steht und"<b>Dune</b>"schon.
Lass in
handleinreading_server.pynach demtry, dasroute()aufruft, und vor der Log-Zeile eineüber response["headers"]laufen und rufeheader_value()für jeden Wert auf, innerhalb einestry. Ein Wert ist ein Text oder einevon Texten: Prüfe jedes Element einer Liste. Ersetze in except (ValueError, TypeError):die Antwort durchjson_error(500, ...)mit der festen Meldung, setze ihrcache-controlaufno-storeund leere ihren Body, wenn dieHEADist.Führe
python -m pytest -qaus, bis es mit26 passedendet, und führe dannpython pl_sec.py injectionaus.Fülle in
SECURITY.mddie Zeilen F10 und F11 aus. F10 nennttest_attack_body_cannot_set_the_owner. F11 nennt mindestens einen der anderen vier Tests dieses Kapitels. Beide sagenclosed in 7.8.
Klicke dann auf Check my work. Es wiederholt die Prüfungen der Lektionen 1, 2 und 4 bis 7 mit neuen Werten: Bodys, Header-Werte, Weiterleitungsziele, ein eigener Ordner mit Covern, eine Seite, die mit Pythons HTML-Parser gelesen wird, und Formulare. Es setzt anstelle von reading_server.make_etag eine reading_server.route eine Funktion, die einen Header mit einer Liste als Wert zurückgibt: handle() muss mit der festen 500 und no-store antworten, eine 500 loggen und keinen Zeilenumbruch hinauslassen, und eine gewöhnliche Anfrage muss weiterhin mit 200 antworten. Es führt deine fünf Tests dieses Kapitels, über ihren Namen ausgewählt, gegen Server aus, die es anstelle von reading_server.handle und der Funktionen einsetzt, in denen die Schutzmaßnahmen stecken: Jeder Test muss gegen einen Server mit allen Schutzmaßnahmen bestehen, gegen einen Server, dem seine eigene Schutzmaßnahme fehlt, fehlschlagen und gegen einen Server, der alles ablehnt, fehlschlagen. Kein Test darf eine Verbindung öffnen. Es liest die Zeilen F10 und F11 von SECURITY.md; ihre Formulierung wird nicht beurteilt. Nichts aus deinem