Ga naar hoofdinhoud

Cookies afschermen: de oplossing

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.

In bericht_plaatsen staat de regel die de cookie zet. Voeg httponly=True toe:

antwoord = JSONResponse({"sleutel": sleutel})
antwoord.set_cookie(
key="sessie_id",
value=sessie_id,
max_age=60 * 60 * 24 * 30,
httponly=True,
)
return antwoord
  1. Regel 2-7:

    Dezelfde set_cookie als in Sessies, nu met httponly=True erbij. 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​

Verstuur een nieuw bericht, 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. De server herkent je nog steeds: de berichten die je verwijdert, gaan weg. Alleen een script komt niet meer bij de cookie.

Er gaat iets mis​

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: verstuur een nieuw bericht, zodat de server een nieuwe set_cookie stuurt, of verwijder de cookie in het tabblad App en laad de pagina opnieuw.

Opdrachten​

Opdracht 1: Predict - Twee cookies​

Je hebt httponly=True gezet en een nieuw bericht verstuurd. 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 verwijderverzoek 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("/bericht")
async def bericht_plaatsen(
naam: str = Form(...),
bericht: str = Form(...),
sessie_id: str = Cookie(default=""),
):
if not sessie_id:
sessie_id = secrets.token_hex(16)

sleutel = f"bericht_{time.time_ns()}"
with SqliteDict("gastenboek.db") as db:
db[sleutel] = {"naam": naam, "bericht": bericht}
db.commit()

with SqliteDict("sessies.db") as sessies:
mijn = sessies.get(sessie_id, {"naam": naam, "berichten": []})
mijn["naam"] = naam
mijn["berichten"].append(sleutel)
sessies[sessie_id] = mijn
sessies.commit()

antwoord = JSONResponse({"sleutel": sleutel})
antwoord.set_cookie(
key="sessie_id",
value=sessie_id,
max_age=60 * 60 * 24 * 30,
httponly=True,
)
return antwoord

@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, {})
if sleutel not in mijn.get("berichten", []):
raise HTTPException(status_code=403, detail="Dit is niet jouw bericht")

with SqliteDict("gastenboek.db") as db:
if sleutel in db:
del db[sleutel]
db.commit()
return {"bericht": "Verwijderd"}

De sessie-cookie staat nu buiten bereik van scripts.

Door naar stap 3: in de praktijk.