Wie mag wat: een knop is geen slot
Hier bouw je op verder: Door een dictionary lopen
Weet je dit niet meer zeker? Lees het terug voordat je verdergaat.
- Python Door een dictionary lopen
In je gastenboek staat bij een bericht een knop Verwijderen. Heb je opdracht 4 van Sessies gemaakt, dan staat hij alleen bij je eigen berichten, en dat voelt als beveiliging. Maar een knop doet maar één ding: een verzoek versturen. Wie dat verzoek zelf verstuurt, heeft de knop niet nodig, verborgen of niet.
Maakte je ook opdracht 5 van Sessies, dan heb je dit gat in je eigen gastenboek al gedicht. Deze reeks laat zien waarom die controle nodig was: de server van deze map heeft hem nog niet.
Doe het na, op je eigen server
Laat de server uit les 1
draaien, en maak in dezelfde map een scriptje test_verwijderen.py. Het speelt
twee bezoekers: Sara en Alex. Allebei maken ze een account, loggen ze in en
plaatsen ze een bericht. Daarna verwijdert Alex dat van Sara.
import httpx
adres = "http://127.0.0.1:8000"
sara = httpx.Client(base_url=adres)
alex = httpx.Client(base_url=adres)
sara.post("/registreer", data={"naam": "Sara", "wachtwoord": "zon123"})
sara.post("/inloggen", data={"naam": "Sara", "wachtwoord": "zon123"})
alex.post("/registreer", data={"naam": "Alex", "wachtwoord": "maan456"})
alex.post("/inloggen", data={"naam": "Alex", "wachtwoord": "maan456"})
sara.post("/bericht", data={"bericht": "Hoi allemaal"})
alex.post("/bericht", data={"bericht": "Leuk gastenboek"})
for sleutel, bericht in alex.get("/berichten").json().items():
if bericht["naam"] == "Sara":
antwoord = alex.delete(f"/bericht/{sleutel}")
print(f"Alex verwijdert het bericht van Sara: {antwoord.status_code}")
namen = []
for bericht in alex.get("/berichten").json().values():
namen.append(bericht["naam"])
print(f"Nog in het gastenboek: {namen}")
- Regel 5-6:
Twee
httpx.Clients zijn twee bezoekers, elk met hun eigen cookies en dus een eigen sessie, zoals in Een bezoeker die onthoudt. - Regel 8-11:
Elk een account en een inlog. Na het inloggen bewaart elke
Clientzijn eigen cookiesessie_id. - Regel 16-19:
Alex vraagt de berichten op, zoals elke bezoeker kan, en stuurt voor het bericht van Sara een
DELETE:alex.deletedoet dat, zoalsalex.posteen POST stuurt. De knop in de browser verstuurt hetzelfde verzoek. - Regel 21-24:
Van elk bericht dat over is alleen de naam, want de sleutels zijn bij jou anders.
Draai het in een tweede terminal.
Alex hoefde niets te raden of te kraken: hij logde
in als zichzelf, en de sleutel stond in het antwoord van /berichten. Het endpoint deed wat Alex vroeg.
Je server weet precies wie Alex is: de naam staat in zijn sessie. Alleen vraagt
bericht_verwijderen er niet naar. Een verborgen knop, zoals in opdracht 4 van
Sessies, maakt dat alleen minder zichtbaar.
Er gaat iets mis
Twee keer Alex, of meer namen
Het script print Nog in het gastenboek: ['Alex', 'Alex'], of er staan namen
bij die je niet kent.
Oorzaak: de databases van een vorige keer staan er nog. Draai je het script een tweede keer, dan plaatst Alex opnieuw een bericht, naast dat van de eerste keer. Start je de server in een andere map, zoals je eigen project, dan ziet het script ook de berichten van daar.
Oplossing: werk in de map veiligheid-toegang uit
les 1, en gooi
gastenboek.db, sessies.db en gebruikers.db weg voordat je het script
opnieuw draait.
Zoek eerst zonder computer bij zes verzoeken de controle die de server beschermt: Vertrouw de bezoeker niet. Je docent kan de hand-out printen.
Opdrachten
Opdracht 1: Predict - Zonder browser
Een derde script heeft geen account, is nooit ingelogd en gebruikt geen
Client. Het kent de sleutel van een bericht en stuurt alleen dit:
import httpx
sleutel = "bericht_1767000000000000000"
antwoord = httpx.delete(f"http://127.0.0.1:8000/bericht/{sleutel}")
print(antwoord.status_code)
Vraag: als dat bericht bestaat, wat print het script?
Tip
Kijk welke parameters bericht_verwijderen heeft. Komt daar een cookie in voor?
Antwoord
200. Het endpoint kijkt alleen naar de sleutel in het pad. Wie het verzoek
stuurt, ingelogd of niet, maakt voor deze server niets uit. Plaatsen kan alleen
met een account; verwijderen kan iedereen.
Opdracht 2: Investigate - Waar staat de sleutel?
Kijk naar de HTML van het recept Verwijderen in het
htmx-overzicht: het stukje met
<li id="bericht-{{ sleutel }}">, tot en met de knop. Heb je dat recept gebouwd, open dan ook je
gastenboek en kijk in het tabblad Elementen naar een bericht. Waar staat de
sleutel? Wat betekent dat voor een idee als "maak de sleutels heel lang, dan
raadt niemand ze"?
Antwoord
De sleutel staat vier keer bij elk bericht: in id="bericht-…", in de link
naar de detailpagina href="/bericht/…", en in de knop bij hx-delete en
hx-target. Ook als je de knop verbergt, staat hij in het id en in de link,
en /berichten geeft hem ook terug. Een lange sleutel is dus geen slot: je hoeft
hem niet te raden, want de server geeft hem zelf. De controle moet in het
endpoint, en dat doe je in les 3.
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 sqlitedict import SqliteDict
app = FastAPI()
@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)
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):
with SqliteDict("gastenboek.db") as db:
if sleutel not in db:
raise HTTPException(status_code=404, detail="Dit bericht bestaat niet")
del db[sleutel]
db.commit()
return {"bericht": "Verwijderd"}
Dit is de zwakke versie: bericht_verwijderen vraagt niet wie er komt.
Door naar les 3: de controle met 403.