0%

Invoer die code wordt · eindproject

Eindproject: Project: niets gaat ongecontroleerd over de grens

Dit hoofdstuk heeft één regel in twee helften. Controleer een body op de heenweg, met een allowlist en limieten. Bewaak elke plek waar tekst naar buiten gaat, naar iets dat wordt geparset: een header, een , een bestandspad, een pagina. Geen van beide helften vervangt de andere. <b>Dune</b> is een geldige titel, en moet in de pagina toch ge-escapet worden.

De validator en de vier bewakingen zijn geschreven. Twee van de vijf hebben een aanvalstest. Dit project schrijft de andere drie tests, voegt één bewaking toe op alles wat handle() verlaat, en sluit twee bevindingen.

Nog drie aanvalstests

Elke test voert een aanval uit en controleert dat die wordt geweigerd, zoals in les 1 en 4. Elke test controleert ook dat een eerlijk verzoek werkt, zodat hij niet kan slagen tegen een server waarop niets werkt.

test_attack_next_cannot_leave_the_site. Loop over drie van next: %2F%2Fother.example%2Fwelcome, %2F%5Cother.example en https:%2F%2Fother.example%2F. Log voor elk ervan ana in met een nieuwe state en controleer met een assert dat de status 303 is en dat location precies / is. De 303 is de eerlijke helft: de login werkte.

test_attack_cover_name_cannot_leave_the_folder. Deze test brengt zijn eigen map mee. Pytest geeft elke testfunctie met een met de naam tmp_path een nieuwe tijdelijke map, als Path. Begin zo:

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

Controleer daarna 200 voor GET /covers/kindred.txt. Controleer 404 voor GET /covers/..%2fsecret.txt, en dat b"not a cover" niet in die body staat.

test_attack_title_cannot_become_markup. Maak met steps.keyed_state() en een token voor ana een boek met de titel <b>Dune</b> en controleer 201. Haal haar op met steps.login_cookie(state, "ana", "correct horse"), stuur GET / met die cookie in een header Cookie, en controleer 200. Decodeer de body, en controleer dat "<b>" niet in de tekst staat en "&lt;b&gt;Dune&lt;/b&gt;" wel.

Alle drie slagen zodra je ze schrijft, omdat de bewakingen er al zijn. Dat bewijst weinig. Zie elke test een keer falen: zet zijn bewaking met de hand uit, voer python -m pytest -q uit, en zet hem weer aan.

  • Zet return value als eerste regel in safe_redirect_target.

  • Zet return pathlib.Path(folder) / name als eerste regel in safe_cover_path.

  • Verander in render_page html.escape(book["title"]) in book["title"].

Elke keer eindigt de run met één fout, en dat is de test voor die bewaking.

Check my work doet hetzelfde, maar grondiger. Het draait elk van je vijf tests tegen een eigen server die elke bewaking heeft, waar de test moet slagen. Daarna draait het de test tegen een server waar alleen die ene bewaking ontbreekt, waar de test moet falen.

Je test voor de coverroute slaagt. Check my work zet er een server voor in de plaats waarvan de coverroute niets controleert, en je test slaagt nog steeds. Wat zegt je dat?

Eén bewaking op alles wat vertrekt

redirect() bewaakt één header. Je server schrijft er meer, zoals etag, allow en set-cookie, en volgende maand bouwt een nieuwe branch er misschien een uit iets dat een verzoek stuurde. Elk antwoord gaat door handle(), dus één daar dekt ze allemaal:

    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):

Een headerwaarde is één tekst of een met teksten, en http_adapter schrijft elk item van een lijst als een eigen regel. Daarom maakt de lus van een losse tekst een lijst met één item en controleert hij elk item. De except noemt ook : een branch die een getal schrijft, zoals 0, laat header_value die opwerpen, en die fout hoort in dezelfde vaste 500 te eindigen.

Jij schrijft de branch except. Die vervangt het antwoord door de vaste 500 uit hoofdstuk 6: json_error(500, ...) met dezelfde melding, cache-control ingesteld op no-store, en een lege body als de HEAD is. Zet het hele blok na de try die route() aanroept en vóór de logregel, zodat het log de status toont die werd verstuurd.

Dit is dicht falen (fail closed), de regel uit hoofdstuk 5, toegepast op uitvoer. Een headerfout in een of andere branch wordt een fout die je in het log kunt zien, en geen antwoord met een regel die iemand anders schreef. De lus controleert alleen headers. Een body wordt gecontroleerd door de code die hem schrijft, zoals render_page doet.

Mijlpalen

  1. De drie tests, elk een keer zien falen met zijn bewaking uitgezet. Met alle bewakingen terug eindigt python -m pytest -q met 26 passed.

  2. De lus in handle(). De tests eindigen nog steeds met 26 passed.

  3. Het hele scenario:

python pl_sec.py injection

Lees alle vier de delen. Het resultaat zegt:

--- result ---
0 of 4 attacks worked
  1. De bevindingen. Vul in SECURITY.md de rijen F10 en F11 in, in je eigen woorden. F10 is de body die de eigenaar van een boek zette, en de test ervoor is test_attack_body_cannot_set_the_owner. F11 is tekst uit een verzoek die als instructies werd gelezen in een header, een doorverwijzing, een pad en een pagina, en die noemt de andere vier tests. Zet beide op closed in 7.8.

Wat nog open staat

Kijk nog een keer naar het verwijderformulier. POST /books/delete wordt alleen door de sessiecookie geautoriseerd, en niets in het verzoek laat zien dat het van jouw pagina kwam. Hoofdstuk 8 bouwt een klein model van een browser en gebruikt dat om dat formulier vanaf de pagina van iemand anders te versturen.

Opdracht

Schrijf de laatste drie aanvalstests van het hoofdstuk, bewaak elke header die handle() verlaat, en sluit twee bevindingen.

  1. Schrijf in tests/test_attacks.py, onder het commentaar voor hoofdstuk 7, drie tests. Bereik de server via reading_server.handle(...).

    • test_attack_next_cannot_leave_the_site(): log voor elke next in %2F%2Fother.example%2Fwelcome, %2F%5Cother.example en https:%2F%2Fother.example%2F ana in met POST /login?next= en die , en controleer met een assert dat de status 303 is en dat response["headers"]["location"] == "/".

    • test_attack_cover_name_cannot_leave_the_folder(tmp_path): maak tmp_path / "covers" met een bestand kindred.txt, schrijf secret.txt naast de map, zet state["covers_dir"] op de map, controleer 200 voor GET /covers/kindred.txt, en controleer 404 voor GET /covers/..%2fsecret.txt en dat de tekst van het geheim niet in de body staat.

    • test_attack_title_cannot_become_markup(): maak met steps.keyed_state() en steps.bearer_for(state, "ana", "correct horse") een boek met de titel <b>Dune</b> en controleer 201; haal GET / op met de uit steps.login_cookie(state, "ana", "correct horse") en controleer 200; controleer dat "<b>" niet in de tekst van de pagina staat en "&lt;b&gt;Dune&lt;/b&gt;" wel.

  2. Loop in handle in reading_server.py, na de try die route() aanroept en vóór de logregel, over response["headers"] en roep header_value() aan op elke waarde, binnen een try. Een waarde is één tekst of een met teksten: controleer elk item van een lijst. Vervang in except (ValueError, TypeError): het antwoord door json_error(500, ...) met de vaste melding, zet zijn cache-control op no-store, en maak zijn body leeg als de HEAD is.

  3. Voer python -m pytest -q uit tot het eindigt met 26 passed, en voer daarna python pl_sec.py injection uit.

  4. Vul in SECURITY.md de rijen F10 en F11 in. F10 noemt test_attack_body_cannot_set_the_owner. F11 noemt minstens een van de andere vier tests van dit hoofdstuk. Beide zeggen closed in 7.8.

Druk daarna op Check my work. Het herhaalt de controles van les 1, 2 en 4 tot en met 7 met nieuwe waarden: bodies, headerwaarden, doorverwijzingsdoelen, een eigen map met covers, een pagina gelezen met de HTML-parser van Python, en formulieren. Het zet een waarvan de waarde een regeleinde bevat in de plaats van reading_server.make_etag, en een functie die een header met een lijst als waarde teruggeeft in de plaats van reading_server.route: handle() moet de vaste 500 met no-store antwoorden, een 500 loggen en geen regeleinde naar buiten laten, en een gewoon verzoek moet nog steeds 200 antwoorden. Het draait je vijf tests van dit hoofdstuk, op naam gekozen, tegen servers die het in de plaats zet van reading_server.handle en van de functies waarin de bewakingen zitten: elke test moet slagen tegen een server met elke bewaking, falen tegen een server waar zijn eigen bewaking ontbreekt, en falen tegen een server die alles weigert. Geen enkele test mag een verbinding openen. Het leest de rijen F10 en F11 van SECURITY.md; hun formulering wordt niet beoordeeld. Niets uit je wordt beoordeeld.