0%

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 , aan elk antwoord dat je code maakt. Zoals in elk hoofdstuk komt de aanval eerst, als test.

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 vooraf: de openbare route antwoordt 200. Voer 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 -tekst bevat, en dat een gemiste escape een scripttag die /covers/kindred.txt noemt in je pagina laat. De policy uit les 4 staat dat toe, omdat het bestand van de eigen van de pagina komt. Zonder 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 zou iedereen op het pad hem kunnen toevoegen of weghalen, dus negeert een browser hem daar, en de standaard (RFC 6797) zegt hem daar niet te sturen. Daarom neemt 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- van hosts, en het woord 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 niet door te geven: 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:

HeaderWaardeWanneer
x-content-type-optionsnosniffaltijd
referrer-policystrict-origin-when-cross-originaltijd
content-security-policyPAGE_CSPhet mediatype is text/html
cache-controlno-store, ter vervanging van elke andere waardehet mediatype is text/html, of het antwoord heeft een header set-cookie
strict-transport-securityHSTShttps 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 zet nooit opgeslagen, en de pagina toont de boeken van één lezer en hun formuliertoken. 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

  1. Schrijf de test hierboven en zie hem falen.

  2. Schrijf harden in reading_server.py. PAGE_CSP en HSTS staan al bovenaan het bestand. Geef het antwoord terug.

  3. Verander de laatste regel van handle() in return harden(response, https=state["https"]). sample_state() heeft nu een sleutel "https", en die is False: je server spreekt nog gewone HTTP. Een service achter een frontserver die afhandelt, zou dezelfde vlag zetten vanuit zijn configuratie.

  4. Verander de laatste regel van closing_error() in return harden(response, https=False). Die functie beantwoordt een verzoek waarvan de body niet gelezen kan worden, zoals een POST zonder Content-Length. Haar antwoord wordt gemaakt voordat handle() draait, dus ze heeft een eigen weg door harden() nodig. Ze heeft geen state om te raadplegen, dus geeft ze False mee.

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 roept reading_server.handle() aan voor GET /about, voor een pad dat niet bestaat en voor een login met een verkeerd wachtwoord, en assert dat elk antwoord x-content-type-options gelijk aan nosniff heeft. Lees de header met response["headers"].get("x-content-type-options"), zodat een ontbrekende header de assert laat falen in plaats van op te werpen. Assert eerst dat GET /about 200 antwoordt.

In reading_server.py:

  • harden(response, *, https) verandert de headers van het antwoord en geeft het antwoord terug:

    • altijd x-content-type-options: nosniff en referrer-policy: strict-origin-when-cross-origin;

    • content-security-policy: PAGE_CSP als het mediatype van content-type text/html is, in hoofd- of kleine letters en met of zonder ;

    • cache-control: no-store, ter vervanging van elke andere , als het mediatype text/html is of het antwoord een header set-cookie heeft;

    • strict-transport-security: HSTS alleen als https waar is. Verder verandert hij niets: niet de status, niet de body en geen andere header.

  • handle() eindigt met return harden(response, https=state["https"]), zodat elk antwoord erdoorheen gaat, fouten inbegrepen.

  • closing_error() eindigt met return harden(response, https=False).

Voer python -m pytest -q uit in de : elke test slaagt.

Klik daarna op Check my work. Die roept harden aan met antwoorden die hij zelf bouwt: , een pagina, antwoorden die zetten, en een antwoord zonder headers. Hij roept 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.