Invoer die code wordt · oefening
Zet tekst in een pagina, geen markup
De leeslijst heeft nu een pagina. GET / toont de boeken van de ingelogde lezer als HTML. Het vierde deel van python pl_sec.py injection slaat een boek op met de titel <b>Dune</b> en drukt twee regels van de pagina af:
--- a page: a book titled <b>Dune</b>, and the rows GET / writes for it ---
POST /tokens, with ana's cookie -> 200
POST /books, with the token -> 201
GET /, with ana's cookie -> 200
b'<span class="title"><b>Dune</b></span> by <span class="author">Frank Herbert</span>'
b'<button aria-label="Delete <b>Dune</b>">Delete</button>'
the title reached the page as markup: a browser would read <b> as a tag
validate_book accepteerde die titel, en terecht: de titel is 11 tekens tekst. In een
Lees de pagina één keer
Open page.html. Je schrijft in deze cursus geen HTML, en vijf dingen zijn genoeg om dit bestand te lezen:
Een tag zoals
<li>begint een element, en</li>beëindigt het. Wat ertussen staat, is de tekst van het element.Een tag kan attributen dragen: een naam, een isgelijkteken en een
tussen dubbele aanhalingstekens, zoals in name="id".Vijf markeringen tussen accolades:
{user}, twee keer{title},{author}en{id}.Twee regels zijn commentaar, dat een parser overslaat. Ze bevatten de woorden
rowenend row. De regels ertussen zijn één boek, en ze worden voor elk boek herhaald.Het
<form>, dat hieronder een eigen sectie heeft.
render_page() vult de markeringen via fill(), dat str.format_map gebruikt. Dat leest de template één keer en leest nooit opnieuw een waarde, dus een titel {author} blijft die tekst. Twee aanroepen van replace(), één per markering, zouden die titel met de naam van de auteur vullen.
Tekst, geen markup
html.escape() vervangt vijf tekens waar een HTML-parser iets mee kan doen. Elk wordt een code die een browser als het teken toont en waar hij nooit iets mee doet:
import html
print(html.escape("<b>Dune</b>"))
Het drukt <b>Dune</b> af. Een browser die dit leest, toont <b>Dune</b> op het scherm en begint geen element. Het ampersand wordt &, het dubbele aanhalingsteken ", en het enkele aanhalingsteken '.
Wat drukt dit af?
import html
print(html.escape('<b>"Tom" & Jerry</b>'))
Escapen is op twee plekken genoeg: tekst tussen tags, en de waarde van een gewoon attribuut tussen dubbele aanhalingstekens, zoals value of aria-label. page.html gebruikt alleen die twee. De titel staat op beide plekken: tussen de tags <span>, en in de aria-label van de knop, een label voor schermlezers. Tussen de aanhalingstekens is het gevaarlijke teken het aanhalingsteken zelf. Een titel " onclick="x zou de waarde beëindigen en een eigen attribuut beginnen. html.escape vervangt aanhalingstekens tenzij je quote=False meegeeft, dus geef dat niet mee. Het is niet genoeg voor een href, want javascript:alert(1) bevat geen van de vijf tekens. Het is niet genoeg voor een attribuut dat een browser als code leest, zoals onclick of style, zelfs tussen dubbele aanhalingstekens: de browser maakt het escapen ongedaan voordat hij de waarde uitvoert. En het is niet genoeg voor iets binnen <script>, of voor een attribuut zonder aanhalingstekens.
Wat dit voorkomt, is een script van de aanvaller dat draait in een pagina die je lezer vertrouwt. Die aanval heet Content-Security-Policy is een tweede verdedigingslinie, en hoofdstuk 10 voegt die toe.
Waar is html.escape niet genoeg?
Eén keer, en op de terugweg
Sla de titel op zoals de client hem stuurde, en escape hem als je HTML schrijft. Escapen op de heenweg zou < in je JSON-antwoorden zetten, waar niets het als naam leest. En een waarde die twee keer ge-escapet is, toont zijn eigen escaping. De eerste ronde verandert & in &. De tweede ronde escapet ook de & van die naam, en de lezer ziet & op de pagina.
Er is dus één plek voor: render_page, waar tekst HTML wordt. Dit is nog een keer de regel uit les 3. De verdediging hoort waar de tekst wordt gebruikt.
Een titel Tom & Jerry wordt ge-escapet bij het opslaan en opnieuw als de pagina wordt geschreven. Wat ziet de lezer?
Het verwijderformulier
Elke rij heeft een formulier met een verborgen id en een knop. Als een lezer op de knop drukt, stuurt de browser POST /books/delete met content-type: application/x-www-form-urlencoded, de body id=1 en de
Die body heeft de vorm van een querystring, dus parse_qs leest hem en decodeert de procenttekens in elke waarde. form_fields(request) kwam mee met het project van dit hoofdstuk, en hij neemt de eerste waarde van elk veld zonder er nog eens naar te kijken. Jij maakt hem op twee manieren strikt. Het mediatype moet het formuliertype zijn. En een veld dat twee keer voorkomt, wordt geweigerd, want code die de eerste waarde controleert en op de laatste handelt, is een klassieke manier waarop twee delen van een programma het oneens worden.
Eén ding om op te merken voordat je verdergaat: niets in dit verzoek bewijst dat de lezer op de knop op jouw pagina drukte. Daar begint hoofdstuk 8.
Een formulierbody is id=7&id=41. Waarom weigert form_fields hem?
Je taak
Stuur in render_page elke waarde één keer door html.escape(), vóór fill(): de gebruiker, elke titel, elke auteur, en elke id als tekst, str(book["id"]).
Herschrijf form_fields(request):
Werp
ValueError("send the form as application/x-www-form-urlencoded")op tenzijmedia_type(request) == FORM_TYPE.Decodeer de body als
en lees hem met parse_qs(text, keep_blank_values=True), zodat een lege waarde bewaard blijft. Je kunt ookstrict_parsing=text != ""enmax_num_fields=20meegeven, die een misvormde en een heel lange body weigeren. Een body die geen UTF-8 is, laatdecodeUnicodeDecodeErroropwerpen, en dat is ook een. Werp
ValueError(f"the field {name} is repeated")op als een veld meer dan één waarde heeft.
Antwoord in delete_from_form eerst json_error(415, ...) als het mediatype niet het formuliertype is, zoals de JSON-routes doen. Roep daarna form_fields aan binnen try, en antwoord json_error(400, str(problem)) als die ValueError opwerpt.
helpers.form_request(target, fields, headers) bouwt een formulier-POST zoals die van de browser, en fields mag een steps.login_cookie:
python -c "import sys; sys.path.insert(0, 'tests'); import helpers, steps, reading_server; s = reading_server.sample_state(); c = steps.login_cookie(s, 'ana', 'correct horse'); r = helpers.form_request('/books/delete', [('id', '1'), ('id', '2')], {'Cookie': c}); print(reading_server.handle(r, s)['status'])"
Het drukt 400 af.
python -m pytest -q eindigt met 23 passed, en het laatste deel van het scenario luidt:
b'<span class="title"><b>Dune</b></span> by <span class="author">Frank Herbert</span>'
b'<button aria-label="Delete <b>Dune</b>">Delete</button>'
the title reached the page as text
--- result ---
0 of 4 attacks worked
Druk daarna op Check my work. Het leest je pagina met de HTML-parser van Python, die geen browser is. De validator en vier bewakingen staan nu. Twee van de vijf hebben een aanvalstest, en het project schrijft de andere drie.
Opdracht
Schrijf elke
In reading_server.py:
render_page(books, user)stuurt elke waarde precies één keer doorhtml.escape()vóórfill(): de gebruiker, en de titel, auteur en id van elk boek (str(book["id"])). Er wordt niets ge-escapet waar het wordt opgeslagen.form_fields(request)geeft de velden van een formulierbody terug als eenmet tekst, of werpt op: ValueError("send the form as application/x-www-form-urlencoded")tenzijmedia_type(request) == FORM_TYPE;de body gedecodeerd als
en gelezen met parse_qs(text, keep_blank_values=True);ValueError(f"the field {name} is repeated")als een veld meer dan één waarde heeft.
delete_from_form(request, state, session)antwoordt eerstjson_error(415, "send the form as application/x-www-form-urlencoded")als het mediatype niet het formuliertype is, enjson_error(400, str(problem))alsform_fieldsValueErroropwerpt. De rest van deblijft zoals hij is.
Voer python -m pytest -q en python pl_sec.py injection uit in de
Druk daarna op Check my work. Het roept render_page() aan met boeken en een gebruiker die je niet hebt gezien, waarvan de teksten tags, ampersands en beide soorten aanhalingstekens bevatten, en leest de pagina met de HTML-parser van Python: geen enkele waarde mag een element of een attribuut worden, elke waarde moet precies terugkomen zoals hij werd gegeven, en de pagina houdt één rij en één formulier per boek. Het roept form_fields() aan met eigen request-dicts: een herhaald veld, een ValueError opwerpen; een gecodeerde waarde, een lege waarde, een lege body en tekst buiten ASCII moeten gelezen worden; een body die geen UTF-8 is, moet een exception opwerpen. Het roept handle() aan met een eigen POST /books/delete en voor GET /. Niets uit je terminal wordt beoordeeld.