Ga naar hoofdinhoud

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.

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:

Ontwikkelaarstools, tabblad Console geopend
Je typt: document.cookie
De Console antwoordt: ''

In het tabblad App staat sessie_id er nog, en in de kolom HttpOnly staat nu een vinkje:

Ontwikkelaarstools, tabblad App geopend
NaamWaardeHttpOnlySameSite
sessie_id85aa1aca2a96c3430cce6accaeefa4db✓Lax

De server herkent je nog steeds: de berichten die je verwijdert, gaan weg. 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.

Waar de controle zit: cookies
  1. Hier zit de controleServer: De server zet de sessie-cookie. Met httponly=True in set_cookie. · httponly

  2. Browser: De browser bewaart hem. In het tabblad App staat een vinkje bij HttpOnly.

  3. Browser: Een script vraagt document.cookie. Het krijgt de sessie-cookie niet te zien. · De zwakheid

  4. Server: Bij een verzoek gaat hij wel mee. De browser stuurt hem in de kop Cookie; je server leest hem met Cookie().

  5. Browser: Het antwoord komt terug. De server heeft je herkend, zonder dat een script het sessie-id zag.

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 les 3: samesite.