Ga naar hoofdinhoud

Wie mag wat: de zwakheid

Hier bouw je op verder

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"}
  1. 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.

  2. Regel 35-38:

    Alle berichten, met hun sleutels. Op de pagina ziet elke bezoeker ze ook.

  3. 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​

Alleen 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}")
  1. Regel 5-6:

    Een httpx.Client werkt 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. Met base_url hoef je daarna alleen nog het pad te typen.

  2. 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.

  3. 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.