Wie mag wat: de zwakheid
Hier bouw je op verder
- Python Door een dictionary loopen
In Sessies liet je de verwijderknop alleen zien bij je eigen berichten. Bij de berichten van een ander is hij weg. Dat voelt als beveiliging, maar een knop doet maar één ding: een verzoek versturen. Wie dat verzoek zelf verstuurt, heeft de knop niet nodig.
Het gastenboek als JSON
De server hieronder is het gastenboek uit Sessies, met het endpoint Verwijderen uit het htmx-overzicht. De HTML is eruit: elk endpoint geeft JSON terug, zodat een scriptje kan lezen wat de server antwoordt.
import secrets
import time
from fastapi import Cookie, FastAPI, Form, HTTPException
from fastapi.responses import JSONResponse
from sqlitedict import SqliteDict
app = FastAPI()
@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)
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):
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"}
- Regel 10-33:
Het POST-endpoint uit Sessies. Alleen het antwoord is anders: JSON met de sleutel van het nieuwe bericht, in plaats van een omleiding naar
/berichten. - Regel 35-38:
Alle berichten, met hun sleutels. Op de pagina ziet elke bezoeker ze ook.
- Regel 40-47:
Het DELETE-endpoint uit het htmx-recept. Het vraagt welk bericht weg moet, en niet wie dat vraagt.
Doe het na, op je eigen server
Doe dit uitsluitend met je eigen server op 127.0.0.1. Berichten of
gegevens van een ander verwijderen via een verzoek dat je zelf in elkaar zet, is
strafbaar, ook als de site het toelaat en ook als het "maar een testje" is.
Zet de server in main.py in een nieuwe map en start hem met fastapi dev main.py. Maak in dezelfde map een scriptje test_verwijderen.py. Het speelt
twee bezoekers: Sara en Alex. Allebei plaatsen ze een bericht, en 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("/bericht", data={"naam": "Sara", "bericht": "Hoi allemaal"})
alex.post("/bericht", data={"naam": "Alex", "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 = [bericht["naam"] for bericht in alex.get("/berichten").json().values()]
print(f"Nog in het gastenboek: {namen}")
- Regel 5-6:
Een
httpx.Clientwerkt als een browser: hij onthoudt de cookies die hij terugkrijgt en stuurt ze bij elk volgend verzoek mee. Twee clients zijn dus twee bezoekers, elk met een eigen sessie. Metbase_urlhoef je daarna alleen nog het pad te typen. - Regel 11-14:
Alex vraagt de berichten op, zoals elke bezoeker kan, en stuurt voor het bericht van Sara een
DELETE. De knop in de browser verstuurt hetzelfde verzoek. - Regel 16-17:
Van elk bericht dat over is alleen de naam, want de sleutels zijn bij jou anders.
Draai het in een tweede terminal:
Alex verwijdert het bericht van Sara: 200
Nog in het gastenboek: ['Alex']
Het bericht van Sara is weg. Alex hoefde niets te raden of te kraken: de sleutel
stond in het antwoord van /berichten. Het endpoint deed wat hem gevraagd werd.
De server controleert niet wie iets vraagt. De verborgen knop maakt dat alleen minder zichtbaar.
Er gaat iets mis
Je ziet meer namen dan Sara en Alex, of het script verwijdert twee berichten
van Sara.
Oorzaak: in de map staat nog een gastenboek.db van eerder, met oude
berichten erin. Het script ziet die ook, en verwijdert elk bericht met de naam
Sara.
Oplossing: zet main.py en het script in een nieuwe, lege map, of gooi
gastenboek.db en sessies.db weg voordat je de server start.
Opdrachten
Opdracht 1: Predict - Zonder browser
Een derde script heeft nooit iets geplaatst 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, met of zonder cookie, maakt voor deze server niets uit.
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 }}">. 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 twee keer bij elk bericht: in id="bericht-…" en in
hx-delete="/bericht/…". Ook als je de knop verbergt, staat hij in het id,
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 de volgende stap.
Door naar stap 2: de oplossing.