Hoofdstuk 7 · oefening
Invoer die code wordt
Accepteer alleen wat je verwacht
De leeslijst is voor dit hoofdstuk gegroeid. Een boek kan nu drie velden meer hebben: het jaar waarin het verscheen, hoe ver je ermee bent, en een
{
"title": "Dune",
"author": "Frank Herbert",
"year": 1965,
"status": "reading",
"tags": ["desert", "classic"]
}
Om zulke velden op te slaan, is ook create_book in reading_server.py veranderd. Deze code is nieuw en kwam mee met het project van dit hoofdstuk. Je hebt hem niet zelf geschreven:
def create_book(request, state, owner):
"""Store the body as a new book owned by owner and return the 201 response."""
data = json_body(request)
book_id = state["next_id"]
state["next_id"] += 1
book = {"id": book_id, "owner": owner}
for key in data:
book[key] = data[key]
state["books"][book_id] = book
response = json_response(201, book)
response["headers"]["location"] = f"/books/{book_id}"
return response
De id en owner, en daarna draait de lus, dus een sleutel in de body vervangt wat de server koos. replace_book, dat PUT afhandelt, heeft dezelfde lus.
Schrijf eerst de aanval
Hoofdstuk 6 maakte de eigenaar het ene feit waartegen elke leesactie en elke schrijfactie wordt gecontroleerd. Probeer hem dus te kiezen. Ben heeft een account dat mag schrijven, en een eigen token. Hij post een boek en voegt "owner": "ana" toe aan de body.
Schrijf dat als een test, test_attack_body_cannot_set_the_owner, onder het commentaar voor hoofdstuk 7 in tests/test_attacks.py:
Bouw de state met
steps.keyed_state(), en haal devoor Authorizationvan ben op metsteps.bearer_for(state, "ben", "battery staple").Post een eerlijk boek met dat token en controleer met een assert dat de status 201 is. Zonder deze stap zou een server die elk boek weigert, je test halen.
Post
{"title": "Planted", "author": "Ben", "owner": "ana"}met hetzelfde token. Controleer dat de status 422 is, en dat geen enkel boek instate["books"]de titelPlantedheeft.
Voer hem daarna uit. De optie -k owner kiest de tests met owner in hun naam:
python -m pytest -q -k owner
De run eindigt met 1 failed, 21 deselected, en dit is de regel die faalde:
E assert 201 == 422
De server antwoordde 201 en sloeg het boek op met "owner": "ana". Het verschijnt nu in de lijst van Ana als ze om GET /books vraagt. Ben, die het maakte, krijgt er 404 voor. Met dezelfde lus kan een body ook de id kiezen die een boek toont, of een veld toevoegen dat niemand had gepland, zoals "is_admin": true.
Deze zwakheid heet mass assignment: de server wijst een hele body in één stap toe aan een record, en dus beslist de client over velden die de client nooit mocht zetten.
De aanval vraagt een geldig token dat mag schrijven: Ben is een gebruiker van de dienst, geen vreemde. Niemand anders werkt in je workspace, dus de test speelt beide rollen.
Een lijst van wat geweigerd wordt, mist er altijd één
De eerste oplossing die opkomt, noemt de sleutel die het probleem veroorzaakte:
if "owner" in body:
raise ValueError("owner is set by the server")
Dat stopt de aanval van de test en laat id open. Een lijst van wat geweigerd wordt, heet een denylist. Hij laat alles door wat hij niet noemt, en daar horen ook velden bij die nog niet bestaan.
Een server weigert een body die owner bevat. Volgende maand voegt een ontwikkelaar een veld is_admin toe aan boeken. Wat gebeurt er met een body die het zet?
Een lijst van wat geaccepteerd wordt
Draai de lijst om. Een allowlist noemt wat geaccepteerd wordt, en al het andere wordt geweigerd. Je project heeft er een, als set:
ALLOWED_FIELDS = {"title", "author", "year", "status", "tags"}
De test is key not in ALLOWED_FIELDS. Een veld dat later aan boeken wordt toegevoegd, blijft buiten verzoeken tot iemand besluit het toe te laten.
validate_book(body) past de lijst toe. Hij werpt ValueError("the body must be a JSON object") op voor een body die geen
voor
ownerofidwerpt hijValueError(f"{key} is set by the server")op;voor elke andere sleutel die niet in
ALLOWED_FIELDSstaat, werpt hijValueError(f"{key} is not a field of a book")op.
Als er geen sleutel werd geweigerd, bouwt hij een nieuwe dict, kopieert hij de sleutels en waarden van de body erin, en geeft hij de nieuwe dict terug. Hij geeft nooit de body zelf terug, dus een sleutel die de validator niet las, kan het boek niet bereiken.
Deze verdediging heb je al eens gezien. In
Het model liet die velden zonder een woord vallen. Jouw extra="forbid".
Een onbekend veld laten vallen en het weigeren houden het allebei uit het boek. Wat voegt weigeren toe?
Je taak
De test is geschreven en rood. Ga nu in reading_server.py aan de slag:
Schrijf
validate_book(body)zoals hierboven beschreven.Roep in
book_request_error(request), na de controles die er al staan,validate_book(json_body(request))aan binnentry. Als dieopwerpt, is de fout die je teruggeeft json_error(422, str(problem)). Laatbook_errorstaan: die meldt nog steeds een ontbrekende titel.Loop in
create_booken inreplace_bookovervalidate_book(json_body(request))in plaats van over de body.
De tweede wijziging geldt voor POST en PUT allebei, dus beide schrijfroutes gaan in deze les dicht. De status volgt HTTP with Python, hoofdstuk 5, les 5. Een 400 zegt dat de body niet gelezen kon worden, en deze werd wel gelezen. Een 415 zegt dat het mediatype verkeerd is, en deze is gelabeld als application/json. Een 403 zegt dat het account niet mag schrijven, en dat van Ben mag het wel. De body werd begrepen, en één ding erin is niet acceptabel: dat is 422.
Een body is geldige JSON, correct gelabeld, en bevat "owner": "ana". Welke status past?
Voer python -m pytest -q -k owner opnieuw uit: 1 passed, 21 deselected. Zonder -k owner eindigt het hele bestand met 22 passed.
Deze les verandert met opzet een contract. In HTTP with Python werd een boekbody met een extra veld geaccepteerd en werd het veld genegeerd. Vanaf nu krijgt zo’n body een 422 die het veld noemt.
Druk daarna op Check my work. De juiste sleutels komen nu binnen, en verder niets. Wat er onder die sleutels staat, is nog niet gecontroleerd: een titel van elke lengte, een jaar van elk type. Dat is de volgende les.
Opdracht
Voorkom dat een body van een verzoek velden zet die bij de server horen, en bewijs het met een test.
Schrijf in tests/test_attacks.py, onder het commentaar voor hoofdstuk 7, test_attack_body_cannot_set_the_owner:
bouw de state met
steps.keyed_state(), en haal devoor Authorizationvan ben op metsteps.bearer_for(state, "ben", "battery staple");post een eerlijk boek, zoals
{"title": "Dune", "author": "Frank Herbert"}, viareading_server.handle(helpers.make_request("POST", "/books", book, headers), state), en controleer met een assert dat de status 201 is;post
{"title": "Planted", "author": "Ben", "owner": "ana"}met hetzelfde token, en controleer dat de status 422 is en dat geen enkel boek instate["books"]de titelPlantedheeft.
Voer python -m pytest -q -k owner uit in de
In reading_server.py:
validate_book(body)werptValueError("the body must be a JSON object")op alsbodygeenis. Voor een sleutel ownerofidwerpt hijValueError(f"{key} is set by the server")op. Voor elke andere sleutel die niet inALLOWED_FIELDSstaat, werpt hijValueError(f"{key} is not a field of a book")op. Anders geeft hij een nieuwe dict terug met de sleutels en waarden van de body. Hij geeft de body zelf nooit terug en verandert hem nooit.book_request_error(request)roeptvalidate_book(json_body(request))aan binnentry, na de controles die hij al heeft, en geeftjson_error(422, str(problem))terug als de aanroepopwerpt. create_bookenreplace_booklopen overvalidate_book(json_body(request))in plaats van over de body.
Voer de test opnieuw uit en zie hem slagen.
Druk daarna op Check my work. Het roept validate_book() aan met bodies die je niet hebt gezien: een None; bodies met toegestane sleutels, die gelijk en als nieuwe dict terug moeten komen; sleutels die niet zijn toegestaan, waaronder een die alleen door een hoofdletter of een spatie van een toegestane sleutel verschilt; en owner en id. Elke weigering moet een ValueError zijn waarvan de melding de sleutel noemt. Daarna roept het handle() aan met eigen gebruikers, boeken en tokens: een POST en een PUT waarvan de body owner, id of een onbekende sleutel zet, moeten 422 krijgen en niets veranderen, en schone verzoeken moeten nog steeds 201 en 200 krijgen, met de id van de server en de gebruiker van het token als owner. Als laatste draait het je test op naam tegen vier eigen servers. Hij moet slagen tegen een die weigert wat niet is toegestaan, en falen tegen een die de hele body kopieert, een die alleen id weigert, en een die op elk boek 422 antwoordt. Niets uit je terminal wordt beoordeeld.