HTTPS, certificaten en beveiligingsheaders · oefening
Eén harden() voor elk antwoord
Je kunt nu één browserinstructie lezen, en je service stuurt er geen. Deze les voegt er vier toe, via één
Voeg deze test toe aan tests/test_attacks.py, onder het commentaar voor hoofdstuk 10 aan het eind van het bestand:
def test_attack_every_response_says_nosniff():
"""Every response, an error included, must tell a browser to believe Content-Type."""
state = reading_server.sample_state()
about = reading_server.handle(helpers.make_request("GET", "/about"), state)
assert about["status"] == 200
assert about["headers"].get("x-content-type-options") == "nosniff"
missing = reading_server.handle(helpers.make_request("GET", "/nowhere"), state)
assert missing["status"] == 404
assert missing["headers"].get("x-content-type-options") == "nosniff"
headers = {"Authorization": helpers.basic("ana", "a wrong guess"), "Origin": steps.OWN}
refused = reading_server.handle(helpers.make_request("POST", "/login", headers=headers), state)
assert refused["status"] == 401
assert refused["headers"].get("x-content-type-options") == "nosniff"
Hij vraagt om een openbare route, een pad dat niet bestaat en een login die mislukt. De eerste assert is zijn python -m pytest -q uit en zie hem falen op de tweede assert. De rest van deze les laat hem slagen.
Geloof het label
Een browser raadt soms aan de bytes wat een antwoord is, wat Content-Type ook zegt. Dat raden heet sniffing. Je service geeft hem iets om over te raden. Een cover wordt geserveerd als text/plain, en de tekst ervan komt van wie de cover ook maar aanleverde. Stel dat een cover /covers/kindred.txt noemt in je pagina laat. De policy uit les 4 staat dat toe, omdat het bestand van de eigen nosniff kan een browser het uitvoeren.
X-Content-Type-Options: nosniff vertelt de browser het label te geloven. Daarmee weigert een browser een script uit te voeren waarvan het opgegeven type geen scripttype is. De header kost niets en past bij elk antwoord, dus elk antwoord krijgt hem.
Een cover wordt geserveerd als text/plain en bevat JavaScript-tekst. Een pagina noemt hem in een scripttag. Het antwoord zegt nosniff. Wat doet een browser?
Blijf op HTTPS
Strict-Transport-Security, kortweg HSTS, is een belofte over de toekomst: deze host spreekt HTTPS, minstens max-age seconden lang. Een browser onthoudt dat. Vanaf dan maakt hij van elke http://-link naar deze host https:// voordat hij iets verstuurt. En als een certificaatcontrole voor deze host faalt, wordt van de browser verwacht dat hij geen manier biedt om langs de fout te klikken. Dat maakt de weigeringen die je in les 3 zag definitief.
De header wordt alleen via HTTPS gestuurd. Via gewone harden() een https.
Twee grenzen. Het eerste bezoek is niet beschermd, omdat de browser nog niets te onthouden heeft. Browsers worden voor dat geval geleverd met preload-preload in een HSTS-header hoort bij dat proces. En de meegeleverde HSTS = "max-age=31536000", is één jaar zonder includeSubDomains. Dat woord zou elke host onder reading.example binden, en notes.reading.example wordt door andere mensen gerund.
Waarom voegt harden() strict-transport-security alleen toe als https waar is?
De browser van een lezer heeft de site nog nooit benaderd. Welk verzoek beschermt HSTS niet?
Zeg minder tegen andere sites
Als een lezer een link vanaf je pagina volgt, vertelt de browser de volgende site waar de lezer vandaan kwam, in een header Referer. Je service logt paden en neemt een parameter next aan, dus hij vertelt de browser zijn Referrer-Policy: strict-origin-when-cross-origin. Een andere site leert dan de origin en nooit het pad. Huidige browsers doen dit standaard. De header legt het gedrag vast voor oudere browsers.
Waarom niet no-referrer, dat nog minder zegt? Omdat die waarde ook de header Origin verandert. Daaronder stuurt een browser Origin: null met elk formulier dat je eigen pagina’s posten: het verwijderformulier, en de formulieren voor inloggen, uitloggen en de paginagrootte. De controle die je in hoofdstuk 8 schreef, weigert null, en terecht. De strengere waarde zou je eigen pagina’s dus buitensluiten.
Iemand verandert de policy in no-referrer, om nog minder te zeggen. Wat werkt er niet meer?
Eén functie, beslist op basis van het antwoord
harden(response, *, https) krijgt een antwoord en één feit over de verbinding. Hij heeft geen verzoek en geen state, dus elke regel wordt beslist op basis van het antwoord zelf:
| Header | Waarde | Wanneer |
|---|---|---|
x-content-type-options | nosniff | altijd |
referrer-policy | strict-origin-when-cross-origin | altijd |
content-security-policy | PAGE_CSP | het mediatype is text/html |
cache-control | no-store, ter vervanging van elke andere waarde | het mediatype is text/html, of het antwoord heeft een header set-cookie |
strict-transport-security | HSTS | https is waar |
Het mediatype is het deel van content-type vóór een eventuele ;, zonder witruimte eromheen en in kleine letters. Een antwoord zonder content-type heeft er geen.
De rij no-store is een vangnet onder een regel die je al hebt. Sinds hoofdstuk 4 wordt een antwoord dat een harden() laat elke andere cache-control en etag zoals de route ze maakte: GET /about houdt max-age=60, en het lezen met een token houdt private, no-cache.
Je opdracht
Schrijf de test hierboven en zie hem falen.
Schrijf
hardeninreading_server.py.PAGE_CSPenHSTSstaan al bovenaan het bestand. Geef het antwoord terug.Verander de laatste regel van
handle()inreturn harden(response, https=state["https"]).sample_state()heeft nu een sleutel"https", en die isFalse: je server spreekt nog gewone HTTP. Een service achter een frontserver dieafhandelt, zou dezelfde vlag zetten vanuit zijn configuratie. Verander de laatste regel van
closing_error()inreturn harden(response, https=False). Die functie beantwoordt een verzoek waarvan de body niet gelezen kan worden, zoals een POST zonderContent-Length. Haar antwoord wordt gemaakt voordathandle()draait, dus ze heeft een eigen weg doorharden()nodig. Ze heeft geen state om te raadplegen, dus geeft zeFalsemee.
python -m pytest -q
Alle tests slagen, ook de nieuwe. Klik daarna op Check my work. Elk antwoord dat je code maakt, draagt nu zijn instructies, en een ervan, HSTS, is nog nooit verstuurd, omdat de server nog geen HTTPS spreekt. Het project verandert dat.
Opdracht
Voeg de browserinstructies toe aan elk antwoord, in reading_server.py, en bewijs er een met een test.
In tests/test_attacks.py:
Schrijf
test_attack_every_response_says_nosniff. Die roeptreading_server.handle()aan voorGET /about, voor een pad dat niet bestaat en voor een login met een verkeerd wachtwoord, en assert dat elk antwoordx-content-type-optionsgelijk aannosniffheeft. Lees de header metresponse["headers"].get("x-content-type-options"), zodat een ontbrekende header de assert laat falen in plaats vanop te werpen. Assert eerst dat GET /about200 antwoordt.
In reading_server.py:
harden(response, *, https)verandert de headers van het antwoord en geeft het antwoord terug:altijd
x-content-type-options: nosniffenreferrer-policy: strict-origin-when-cross-origin;content-security-policy: PAGE_CSPals het mediatype vancontent-typetext/htmlis, in hoofd- of kleine letters en met of zonder; cache-control: no-store, ter vervanging van elke andere, als het mediatype text/htmlis of het antwoord een headerset-cookieheeft;strict-transport-security: HSTSalleen alshttpswaar is. Verder verandert hij niets: niet de status, niet de body en geen andere header.
handle()eindigt metreturn harden(response, https=state["https"]), zodat elk antwoord erdoorheen gaat, fouten inbegrepen.closing_error()eindigt metreturn harden(response, https=False).
Voer python -m pytest -q uit in de
Klik daarna op Check my work. Die roept harden aan met antwoorden die hij zelf bouwt: handle aan met gebruikers, boeken en een klok die je niet hebt gezien, met state["https"] zowel False als True, en stuurt het eigen verwijderformulier van de pagina zoals een browser dat zou doen. Hij stuurt een POST zonder Content-Length in het geheugen door http_adapter. Hij draait je test tegen een server die de header overal stuurt, waar hij moet slagen, en tegen een server waarvan de foutantwoorden hem missen, waar hij moet falen. Geen enkele controle opent een verbinding. Niets uit je terminal wordt beoordeeld.