Drei P3-Funde aus der Durchsicht vom 14.09.:
Reservieren: Statusprüfung und Schreiben waren getrennt - zwei
gleichzeitige Anfragen konnten beide passieren, die zweite überschrieb
Name und Token der ersten, ohne dass die es erfuhr. Jetzt entscheidet
ein UPDATE mit Status-Bedingung; der Verlierer bekommt None und die
Route meldet "schon weg" (Seite) bzw. 409 (API).
/?limit=abc lieferte jedem anonymen Besucher einen internen
Serverfehler, limit=-1 hiess in SQLite "alles". _limit_lesen() fällt
bei Unsinn auf die Seitengrösse zurück und deckelt bei 500.
Erfassen/Bearbeiten: int(category_id) und die Pydantic-Prüfung warfen
im Handler - 500 statt Fehlermeldung. Jetzt Meldung; ausserdem werden
die Angaben VOR den Bildern geprüft, damit bei abgelehnten Angaben
keine verwaisten Bilddateien liegen bleiben.
Vier neue Tests, jeder einmal gegen den alten Code gelaufen und dabei
rot geworden. 84 lokal grün (HEIC-Test braucht pillow-heif, Docker).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lesen darf jeder, schreiben nur der Betreiber - mit der bewussten Ausnahme
des Reservierens. Dazu Dockerfile und Compose-Datei, damit sich das lokal
ausprobieren lässt.
Beim Bauen sind zwei Fehler aufgefallen, die ohne Test nicht aufgefallen
wären:
1. slowapi zählt pro URL-Pfad. Weil jedes Kleidungsstück eine eigene URL
hat, bekam jedes seinen eigenen Zähler - ein Skript hätte also den
gesamten Bestand reservieren können, ohne je an ein Limit zu stossen.
Genau der Missbrauch, gegen den das Limit gedacht ist. Behoben mit
shared_limit und festem scope.
2. Der erste Anlauf des Tests machte fünf Anfragen gegen ein Limit von
fünf und konnte damit gar nichts zeigen. Geprüft wird jetzt der
tatsächlich ausgelieferte Standardwert, mit mehr Anfragen als erlaubt.
Weiter umgesetzt:
- Passwort als bcrypt-Hash aus der Umgebung, einmal beim Start gebildet
und gemerkt. Bei jeder Anfrage neu gehasht liesse sich die Anwendung
sonst ohne Anmeldung lahmlegen - bcrypt ist absichtlich langsam.
- Ohne hinterlegtes Passwort bleibt der Erfassungsbereich gesperrt (503)
statt offen zu stehen. Kein mitgeliefertes Standardpasswort.
- Sitzung als signiertes Cookie, HttpOnly, SameSite=Lax (blockt
seitenfremde POSTs), Secure abschaltbar nur fürs lokale Testen,
Abmeldung nach zwei Stunden Ruhe.
- CSP mit script-src 'self', nosniff, frame-ancestors none, dazu noindex
und robots.txt: die Galerie ist frei zugänglich, soll aber nicht
dauerhaft im Suchindex stehen.
- Der Betreiber darf Reservierungen ohne Token aufheben, damit sich eine
Missbrauchswelle aufräumen lässt.
- uvicorn mit --proxy-headers: hinter einem Reverse-Proxy zählte sonst
alles auf dessen IP, und ein einzelner Besucher sperrte alle aus.
- Container läuft nicht als root; Code gehört root, nur Daten und Bilder
dem Dienstbenutzer.
Umgebungsvariablen heissen jetzt ausdrücklich englisch (ADMIN_PASSWORD,
SECRET_KEY, ...), passend zur Anleitung und zur Kantone-App.
59 Tests, alle grün. Zusätzlich gegen den laufenden Container geprüft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
FastAPI mit SQLAlchemy und SQLite, Migrationen über Alembic. Die
Endpunkte aus projekt.md für items, categories und sizes stehen samt
Reservierung; Bilder und Anmeldung folgen in Phase 2 und 3.
Umgesetzt wie in Plan.md festgelegt:
- Feste Wertelisten als Enum UND als CHECK in der Datenbank. Die
CHECK-Bedingung ist der eigentliche Schutz: an SQLAlchemy vorbei (Import,
sqlite3 von Hand) käme sonst "Gril" durch, und die Filter griffen still
nicht mehr.
- Status "draft" für den Stapel-Import. Entwürfe erscheinen weder in der
Galerie noch in GET /items, solange nicht ausdrücklich status=draft
angefragt wird, und lassen sich nicht reservieren.
- Titel ist freiwillig und wird sonst beim ANZEIGEN aus Kategorie und
Grösse gebildet ("Jacken 98/104") - nicht beim Speichern, damit er einer
späteren Korrektur der Grösse folgt.
- reservation_token: nur wer es hat, kann die eigene Reservierung aufheben.
Verglichen mit compare_digest, gelöscht beim Freigeben und beim
Erledigen, damit ein alter Link nicht später eine fremde Reservierung
aufhebt.
- Eigene sizes-Tabelle mit sort_order statt SELECT DISTINCT: sonst stünde
"104" vor "56" und jeder Tippfehler würde zur Filteroption.
- Pagination auf GET /items, in projekt.md nicht vorgesehen.
Zwei SQLite-Eigenheiten, die leicht untergehen: foreign_keys ist
standardmässig AUS (ohne PRAGMA greift ON DELETE CASCADE nicht), und
check_same_thread muss für FastAPI abgeschaltet werden. Beides in
database.py, dazu WAL fürs gleichzeitige Lesen.
24 Tests, alle grün. Zusätzlich von Hand geprüft: Migration anwenden,
Eintrag anlegen, reservieren, ohne Token freigeben (403), mit Token
freigeben (200), Swagger UI erreichbar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o