Cookies afschermen: httponly
De browser kan een cookie voor scripts afschermen. Dan stuurt hij hem nog wel
bij elk verzoek mee, zodat je server de sessie herkent, maar document.cookie
laat hem niet meer zien. Je zet het aan met één instelling in set_cookie:
httponly.
httponly in set_cookie
In inloggen staat de regel die de cookie zet. Voeg httponly=True
toe:
antwoord = JSONResponse({"bericht": f"Ingelogd als {naam}"})
antwoord.set_cookie(
key="sessie_id",
value=sessie_id,
max_age=60 * 60 * 24 * 30,
httponly=True,
)
return antwoord
- Regel 2-7:
Dezelfde
set_cookieals in Sessies, nu methttponly=Trueerbij. De naam, de waarde en de houdbaarheid blijven gelijk.
De naam httponly betekent: alleen voor HTTP-verzoeken, niet voor JavaScript.
De cookie gaat dus wel over het netwerk mee, maar een script op de pagina kan
hem niet lezen.
Doe de test opnieuw
Log opnieuw in, zodat je een verse cookie krijgt. Open de Console op
127.0.0.1:8000 en typ weer document.cookie. Nu antwoordt de Console met een
lege tekst:
In het tabblad App staat sessie_id er nog, en in de kolom HttpOnly
staat nu een vinkje:
| Naam | Waarde | HttpOnly | SameSite |
|---|---|---|---|
sessie_id | 85aa1aca2a96c3430cce6accaeefa4db | ✓ | Lax |
De browser stuurt de cookie nog steeds mee. Verstuur nog een bericht en klik in
het tabblad Netwerk op het verzoek bericht: onder Verzoekheaders staat
de regel Cookie met je sessie-id. Alleen een script komt niet meer bij de
cookie.
Waar de controle zit
Hier zit de controle niet in een endpoint, maar in de instelling waarmee de server de cookie zet. De browser houdt zich eraan.
Hier zit de controleServer: De server zet de sessie-cookie. Met httponly=True in set_cookie. · httponly
Browser: De browser bewaart hem. In het tabblad App staat een vinkje bij HttpOnly.
Browser: Een script vraagt document.cookie. Het krijgt de sessie-cookie niet te zien. · Scripts lezen cookies
Server: Bij een verzoek gaat hij wel mee. De browser stuurt hem in de kop Cookie; je server leest hem met Cookie().
Browser: Het antwoord komt terug. De server heeft je herkend, zonder dat een script het sessie-id zag.
Er gaat iets mis
Je oude sessie-id staat er nog
document.cookie toont je oude sessie-id nog, ook al heb je httponly=True
gezet.
Oorzaak: dit is nog de cookie van vóór de wijziging. httponly geldt vanaf
het moment dat de server de cookie zet, en de oude staat al in je browser.
Oplossing: log opnieuw in, zodat de server een nieuwe set_cookie stuurt.
Verwijder je de cookie in het tabblad App, dan ben je uitgelogd, en krijg je
de nieuwe cookie ook bij de volgende keer inloggen.
Andere cookies dan sessie_id
document.cookie is niet leeg, maar geeft bijvoorbeeld:
'naam=sara; volgorde=nieuw'
Oorzaak: een cookie hoort bij het adres 127.0.0.1, niet bij de map of de
poort. De cookies van eerdere projecten, zoals naam uit
Onthouden met een cookie, staan dus nog in je
browser. Ze hebben geen httponly, en een script ziet ze. Je sessie_id staat
er niet bij: die is wel afgeschermd.
Oplossing: verwijder naam, volgorde en de andere oude cookies in het
tabblad App, onder Cookies bij http://127.0.0.1:8000. Of open de pagina in
een privévenster en log daar opnieuw in: daar begint de browser zonder cookies.
Opdrachten
Opdracht 1: Predict - Twee cookies
Je hebt httponly=True gezet en opnieuw ingelogd. Op de pagina met
het formulier typ je in de Console eerst:
document.cookie = "thema=donker"
Die regel maakt een tweede cookie, zonder httponly. Daarna typ je weer
document.cookie.
Vraag: wat antwoordt de Console nu?
Tip
httponly geldt per cookie, niet voor alle cookies van de site.
Antwoord
'thema=donker'. Het script ziet de cookie zonder httponly, en de
sessie-cookie niet. In het tabblad App staan ze allebei, sessie_id met
een vinkje bij HttpOnly en thema zonder.
Opdracht 2: Investigate - Waarom stuurt de browser hem nog mee?
Met httponly kan een script de cookie niet lezen. Toch weet je server na een
bericht nog wie je bent. Hoe kan dat?
Antwoord
httponly schermt de cookie alleen af voor scripts. De browser zelf stuurt hem
bij elk verzoek naar 127.0.0.1:8000 mee, in de kop Cookie. Je server leest
hem daaruit met sessie_id: str = Cookie(default=""), net als eerst. Alleen
JavaScript is buitengesloten.
Zo ziet je main.py er nu uit
import secrets
import time
from fastapi import Cookie, FastAPI, Form, HTTPException
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlitedict import SqliteDict
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.post("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as gebruikers:
if naam in gebruikers:
raise HTTPException(status_code=400, detail="Deze naam is al bezet")
gebruikers[naam] = wachtwoord
gebruikers.commit()
return {"bericht": f"Welkom, {naam}"}
@app.post("/inloggen")
async def inloggen(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as gebruikers:
opgeslagen = gebruikers.get(naam)
if opgeslagen != wachtwoord:
raise HTTPException(status_code=401, detail="Naam of wachtwoord klopt niet")
sessie_id = secrets.token_hex(16)
with SqliteDict("sessies.db") as sessies:
sessies[sessie_id] = {"naam": naam}
sessies.commit()
antwoord = JSONResponse({"bericht": f"Ingelogd als {naam}"})
antwoord.set_cookie(
key="sessie_id",
value=sessie_id,
max_age=60 * 60 * 24 * 30,
httponly=True,
)
return antwoord
@app.post("/bericht")
async def bericht_plaatsen(bericht: str = Form(...), sessie_id: str = Cookie(default="")):
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})
if not mijn:
raise HTTPException(status_code=401, detail="Log eerst in")
sleutel = f"bericht_{time.time_ns()}"
with SqliteDict("gastenboek.db") as db:
db[sleutel] = {"naam": mijn["naam"], "bericht": bericht}
db.commit()
return {"sleutel": sleutel}
@app.get("/berichten")
async def berichten():
with SqliteDict("gastenboek.db") as db:
return dict(db)
@app.delete("/bericht/{sleutel}")
async def bericht_verwijderen(sleutel: str, sessie_id: str = Cookie(default="")):
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})
with SqliteDict("gastenboek.db") as db:
bericht = db.get(sleutel)
if bericht is None or bericht["naam"] != mijn.get("naam"):
raise HTTPException(status_code=403, detail="Dit is niet jouw bericht")
del db[sleutel]
db.commit()
return {"bericht": "Verwijderd"}
@app.delete("/berichten")
async def alles_wissen(sessie_id: str = Cookie(default="")):
with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {})
with SqliteDict("gastenboek.db") as db:
van_mij = []
for sleutel, bericht in db.items():
if bericht["naam"] == mijn.get("naam"):
van_mij.append(sleutel)
for sleutel in van_mij:
del db[sleutel]
db.commit()
return {"bericht": "Jouw berichten zijn gewist"}
De sessie-cookie staat nu buiten bereik van scripts.
Door naar les 4: samesite.