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
Uploads werden geprüft, nach WebP gewandelt (max. 1200 px Breite) und über
eine eigene Route ausgeliefert. Dazu der Stapel-Import: viele Fotos auf
einmal hochladen, pro Foto entsteht ein Entwurf.
Der Upload ist die grösste Angriffsfläche der Anwendung - die Galerie ist
frei zugänglich, und hier landet fremdes Binärmaterial auf der Platte.
Entsprechend:
- Der Dateiname wird selbst erzeugt (UUID + .webp), nie aus dem Upload
übernommen. Beim Ausliefern wird er zusätzlich gegen ein Muster geprüft:
er kommt dort aus der URL und ist damit Nutzereingabe, auch wenn wir ihn
ursprünglich vergeben haben.
- Der Inhalt entscheidet, nicht die Endung - Pillow muss das Bild öffnen
können. Eine als .jpg getarnte PHP-Datei fällt durch.
- Pixelmasse ausdrücklich begrenzt, VOR dem Dekodieren. Pillow allein
wirft erst bei der doppelten Zahl einen Fehler; dazwischen gibt es nur
eine Warnung, die niemand sieht.
- Gespeichert wird nur das Ergebnis. Das entfernt eingebettete
Fremdinhalte und die EXIF-Daten - bei Handyfotos oft samt
GPS-Koordinaten der Wohnung. Die Drehung wird vorher auf die Bildpunkte
angewendet, sonst lägen Hochkant-Fotos quer.
- Bilder liegen nicht unter static/, sondern werden mit festem
Content-Type und nosniff ausgeliefert.
HEIC wird unterstützt (pillow-heif): iPhones nehmen so auf, und ohne das
fiele beim Stapel-Import jedes iPhone-Foto durch. Ein unlesbares Bild
lässt bewusst den ganzen Stapel scheitern - bei zwanzig Fotos fiele sonst
niemandem auf, dass eines fehlt.
Beim Löschen werden die Dateien mitentfernt (ON DELETE CASCADE räumt nur
die Zeilen weg), und wird das Vorschaubild gelöscht, rückt das nächste
nach.
42 Tests, alle grün. Zusätzlich von Hand gegen die laufende API geprüft:
3000x2200-Foto kommt als WebP 1200x880 an, Pfadmanipulation und getarnte
Dateien werden abgewiesen, Entwürfe bleiben aus der Galerie draussen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o