Sicherheitsprüfung - Ergebnis #20
Notifications
Total Time Spent: 1 hour 3 minutes
florian.wilde
1 hour 3 minutes
No due date set.
Dependencies
No dependencies set.
Reference: florian.wilde/qrcode-generator#20
Reference in New Issue
Block a user
Sicherheitsprüfung: qrcode-generator
Stand: 24. August 2026
Geprüfter Commit:
4e2d222e8611f01ed11b48bd7b1013cfbf6ced53Methode: manuelle statische Codeprüfung, Abhängigkeits-Audit, Secret-Suche, Lint, TypeScript- und Produktions-Build-Prüfung. Kein Test gegen eine laufende Produktionsinstanz.
Kurzurteil
Die Anwendung hat eine überdurchschnittlich sorgfältige Sicherheitsbasis: zentrale serverseitige Autorisierung, parametrisierte Datenbankzugriffe über Prisma, starke Passwort-Hashes, gehashte Sitzungstoken, OIDC mit State/Nonce/PKCE, Upload-Signaturprüfung und sandboxartige Header bei der Dateiauslieferung. Im geprüften Stand ist sie trotzdem nicht bedenkenlos öffentlich bereitzustellen.
Der wichtigste Fehler liegt in der OIDC-Kontoverknüpfung: Ein fehlender
email_verified-Claim wird als bestätigte E-Mail gewertet. Abhängig vom Identity Provider kann das die Übernahme eines bestehenden lokalen Kontos einschließlich eines Administratorkontos ermöglichen. Hinzu kommen gefährliche Auslieferungsvorgaben (AUTH_ENABLED=false), leicht auslösbare Kontosperren, umgehbare In-Memory-Drosselungen und ein Wettlauf beim ersten OIDC-Administrator.Befunde
S1 – Hoch: OIDC verknüpft Konten auch ohne positiv bestätigte E-Mail
Betroffene Stelle:
src/lib/auth/oidc.ts:326-330, verwendet insrc/app/api/auth/oidc/callback/route.ts:94-119.emailVerifiedwird alspayload.email_verified !== falseberechnet. Damit gelten nicht nurtrue, sondern auch ein fehlender Claim,null,0oder eine Zeichenkette als bestätigt. Findet die Callback-Route anschließend ein lokales Konto mit derselben E-Mail-Adresse, hängt sie die fremde OIDC-Identität (sub) an dieses Konto. Danach kann sich diese Identität als das lokale Konto anmelden.Ausnutzbarkeit: Hoch, wenn der konfigurierte OIDC-Anbieter
email_verifiednicht liefert oder Benutzer ihre E-Mail-Adresse dort unzureichend kontrolliert setzen können. Das ID-Token selbst muss weiterhin gültig signiert sein; dies ist kein Angriff ohne Zugriff auf den IdP.Auswirkung: Kontoübernahme, bei passender Adresse auch Instanz-Administratorrechte.
Behebung: Nur den booleschen Wert
trueakzeptieren:Noch robuster: Für die automatische Verknüpfung zusätzlich eine explizite Konfigurationsfreigabe verlangen oder lokale und OIDC-Konten nur administrativ verknüpfen. Einen Test für fehlend,
false,null, String undtrueergänzen.S2 – Hoch (Fehlkonfiguration): Authentifizierung ist standardmäßig ausgeschaltet
Betroffene Stellen:
.env.example:16-22,docker-compose.yml:37-40.Die Compose-Vorgabe startet mit
AUTH_ENABLED=false. In diesem Modus erzeugt die Anwendung einensystem-Akteur, der jede Berechtigung besitzt. Gleichzeitig veröffentlicht Compose Port 3000 auf allen Interfaces. Eine versehentlich öffentlich erreichbare Standardinstallation erlaubt damit jedem Besucher Verwaltung, Uploads sowie Anlegen, Ändern und Löschen aller Codes.Ausnutzbarkeit: Unmittelbar, sobald der Host aus einem nicht vollständig vertrauenswürdigen Netz erreichbar ist.
Behebung: Fail-closed ausliefern:
AUTH_ENABLEDin Compose verpflichtend machen (${AUTH_ENABLED:?…}) oder standardmäßigtruesetzen. Für bewusst offenen Einzelplatzbetrieb eine deutlich benannte Option verwenden und beim Start eine auffällige Warnung protokollieren. Port standardmäßig an127.0.0.1binden oder einen vorgeschalteten TLS-Proxy dokumentiert voraussetzen.S3 – Mittel: Jeder kann bekannte Konten gezielt aussperren
Betroffene Stelle:
src/app/api/auth/route.ts(MAX_ATTEMPTS = 8, 15 Minuten Sperre).Acht falsche Passwörter sperren das betroffene Konto für 15 Minuten. Es gibt keinen Nachweis, dass der Anfragende der Kontoinhaber ist. Ein Angreifer, der die Administratoradresse kennt, kann die Sperre wiederholt erneuern und so die Verwaltung praktisch dauerhaft blockieren. Die zusätzliche IP-Drosselung verhindert dies nicht; acht Versuche liegen unter ihrer Grenze von 20.
Auswirkung: Gezielter Denial-of-Service gegen einzelne Benutzer, besonders problematisch bei nur einem Administrator.
Behebung: Progressive Verzögerung statt harter Kontosperre bevorzugen; alternativ Sperren zusätzlich nach vertrauenswürdig ermittelter Quelladresse begrenzen, Admin-Wiederherstellung vorsehen und Ereignisse alarmieren. Niemals allein auf vom Client gelieferte Forwarded-Header vertrauen.
S4 – Mittel: Drosselungen lassen sich per
X-Forwarded-ForumgehenBetroffene Stellen:
src/lib/auth/throttle.ts:56-68,src/app/api/feedback/route.ts:61-70.Die Anwendung übernimmt stets den ersten Wert aus
X-Forwarded-For. Bei direkter Veröffentlichung (die mitgelieferte Compose-Konfiguration tut genau das) kann jeder Client diesen Header frei wählen. Dadurch lassen sich sowohl die Login-Bremse als auch die öffentliche Feedback-Bremse mit wechselnden Werten umgehen. Die Zähler sind außerdem nur pro Prozess gespeichert und verlieren bei Neustart oder horizontaler Skalierung ihre Wirkung.Auswirkung: Erleichtertes Password-Spraying, Feedback-Spam und Datenbankwachstum. Die Kontosperre begrenzt Versuche pro bekanntem Konto, verhindert aber breites Spraying und Feedback-Fluten nicht zuverlässig.
Behebung: Einen festen Trust-Proxy-Modus einführen: Forwarded-Header nur von bekannten Proxy-Adressen akzeptieren, sonst die Socket-Adresse verwenden. Für mehrere Instanzen einen gemeinsamen, atomaren Zähler (z. B. Redis oder Datenbank) verwenden. Zusätzlich globale Obergrenzen und Body-/Request-Rate-Limits am Reverse Proxy setzen.
S5 – Mittel: Wettlauf erzeugt mehrere erste OIDC-Administratoren
Betroffene Stelle:
src/app/api/auth/oidc/callback/route.ts:122-145.Beim OIDC-Bootstrap werden
user.count() === 0unduser.create()getrennt ausgeführt. Zwei gleichzeitig abgeschlossene erste Anmeldungen können beideistErster=truesehen und anschließend beide als Instanz-Administrator angelegt werden. Die lokale Passwort-Ersteinrichtung schützt denselben Ablauf bereits korrekt mit einer serialisierbaren Transaktion; der OIDC-Pfad tut dies nicht.Ausnutzbarkeit: Nur auf einer frischen Installation und mit einer gültigen IdP-Identität, aber gerade der erste Administrator ist eine besonders empfindliche Vertrauensgrenze.
Behebung: Prüfung und Anlage wie im lokalen Setup in eine serialisierbare Transaktion legen und Serialisierungsfehler sicher wiederholen. Noch besser: Bootstrap durch ein einmaliges, serverseitiges Setup-Token oder eine vorab erlaubte Administratoridentität absichern.
S6 – Niedrig bis mittel: CSRF-/Proxy-Vertrauensmodell ist fragil
Betroffene Stellen:
src/lib/http.ts:18-30,src/lib/auth/session.ts:60-65,src/lib/auth/oidc.ts:106-121.Die Origin-Prüfung vergleicht nur
origin.host, nicht das vollständige Origin-Tupel aus Schema, Host und Port, und bevorzugt ungeprüftX-Forwarded-Host. Auch dasSecure-Flag der Sitzung vertrautX-Forwarded-Proto. Bei einem korrekt konfigurierten Reverse Proxy ist das meist ausreichend; bei direkter Exposition oder einem Proxy, der eingehende Forwarded-Header nicht überschreibt, entscheidet jedoch der Client über sicherheitsrelevante Werte.Ein fehlender
Origin-Header wird pauschal akzeptiert. Für klassische Browser-CSRF-Angriffe ist das oft vertretbar, lässt aber keinen Schutz gegen Clients zu, die den Header entfernen, und erschwert ein eindeutiges Sicherheitsversprechen.Behebung: Explizite Liste vertrauenswürdiger öffentlicher Origins und Proxy-Hops konfigurieren. Für schreibende Cookie-authentifizierte Requests vollständigen Origin-Vergleich oder ein CSRF-Token verlangen. Forwarded-Header nur hinter einem bekannten Proxy auswerten.
Secureprimär aus einer verpflichtenden HTTPS-BASE_URLableiten.S7 – Niedrig: Bekannte High-Advisory in der Prisma-Toolchain
npm audit --omit=devmeldetGHSA-ggr8-5vv4-36mxindeepmerge-ts@7.1.5, transitiv über@prisma/config@7.9.1/prisma@7.9.1. Die Schwachstelle ist Stack-Erschöpfung beim Zusammenführen rekursiver Objektgraphen.Die praktische Remote-Ausnutzbarkeit ist hier niedrig, weil Prisma-Konfiguration nicht aus HTTP-Eingaben erzeugt wird. Die Prisma-CLI wird jedoch in das Laufzeit-Image kopiert und beim Containerstart für Migrationen verwendet; daher sollte der Befund nicht einfach als reine Entwicklungsabhängigkeit ignoriert werden.
Behebung: Auf eine Prisma-Version aktualisieren, die
deepmerge-ts >= 8verwendet, sobald kompatibel verfügbar, und den Lockfile-Audit in CI erzwingen. Kein blindesnpm audit fix --forceauf Prisma 6 durchführen.Härtungsempfehlungen
Positive Beobachtungen
evalgefunden.Prüfnachweise und Grenzen
npm run lint: erfolgreich.npm run build: erfolgreich; der Build führte auch die TypeScript-Prüfung erfolgreich aus.npm run typecheckvor dem Next-Build scheiterte zunächst, weil die von Next generierten globalenLayoutProps-Typen noch fehlten; dies war nach dem Produktions-Build kein Buildfehler.npm audit --omit=dev: drei als „high“ gezählte Paketknoten, eine zugrunde liegende Advisory indeepmerge-ts.Priorisierte Maßnahmen
email_verifiedfail-closed korrigieren und Tests ergänzen.AUTH_ENABLEDfail-closed machen; öffentliche Bindung/TLS-Proxy absichern.🤖 Claude Code hat mit der Umsetzung begonnen.
fix/20-sicherheitspruefungNachgeprüft: alle sieben treffen zu. Besonders S1 — der Kommentar im Code sagt ausdrücklich „Verknüpfen nur bei bestätigter Adresse“, und die Berechnung
payload.email_verified !== falsehebelt genau das aus. Ein fehlender Claim gilt damit als bestätigt.=== true; Bootstrap in eine serialisierbare Transaktion wie bei der lokalen Ersteinrichtung).AUTH_ENABLEDwird fail-closed: Vorgabetrue, und wer sie abschaltet, bekommt beim Start eine deutliche Warnung ins Protokoll.Securekünftig aus dem Schema vonBASE_URLstatt ausX-Forwarded-Proto; die Herkunftsprüfung gegen eine serverseitige Liste (BASE_URL + registrierte Domains) statt gegen den mitgeschickten Host-Header.deepmerge-ts@7.1.5).Zeiterfassung läuft.
✅ Umsetzung abgeschlossen — bereit zum Testen.
Alle sieben Befunde treffen zu. Vier behoben, drei begründet offen.
X-Forwarded-ForumgehbarWas geändert wurde
payload.email_verified === truestatt!== false. Der Kommentar daneben beschrieb die Absicht schon richtig — die Zeile darunter tat das Gegenteil.isAuthEnabled()verlangte den Wert"true", alles andere hieß „aus“. Eine vergessene oder falsch geschriebene Variable ergab eine Anwendung ohne jede Anmeldung. Jetzt umgekehrt, plus Vorgaben auftrueund eine mehrzeilige Startwarnung im Container.BASE_URL.Abweichung von meinem Plan oben: S3 wollte ich beheben, indem während der Sperre weiter geprüft und ein richtiges Passwort durchgelassen wird. Beim Nachrechnen verworfen: Eine Prüfung kostet hier rund 128 MiB (scrypt mit N = 2¹⁷). Wer sie beliebig oft auslösen darf, legt den Server lahm — aus einer Sperre gegen ein Konto würde eine gegen alle. Der Punkt braucht einen eigenen Entwurf, zusammen mit S4.
Geänderte Dateien
src/lib/auth/oidc.ts,src/app/api/auth/oidc/callback/route.tssrc/lib/auth/actor.ts,src/lib/http.tsdocker-compose.yml,.env.example,docker/entrypoint.shdocs/sicherheit.md— vier neue behobene Punkte, drei offene mit BegründungSo testest du es
git switch fix/20-sicherheitspruefung.envstehtAUTH_ENABLED=false. Das bleibt wirksam — an deinem Stack ändert sich nichts außer der Warnung im Protokoll.docker compose up -d --build app, danndocker compose logs app— die Warnung „ACHTUNG: AUTH_ENABLED=false“ muss dastehen..envauskommentieren und neu starten. Erwartet: Die Anwendung verlangt jetzt eine Anmeldung, obwohl gar nichts gesetzt ist.Geprüft: Eigene Reihe zu den Befunden 34 von 34, darunter die Wahrheitstabellen für
email_verifiedundAUTH_ENABLED. Am laufenden Server nachgestellt: mit leeremAUTH_ENABLEDantwortet/mit 307 auf/anmelden,/api/codesmit 401. Bestehende Reihen:sicherheit21/21 (frische Datenbank, Anmeldung an),dubletten24/24,filter-art28/28,domain-check38/38,logo44/44.Aufgewendete Zeit: 00:18
Pull Request: #21
Nach Freigabe:
/issue-freigabe 20Am Rande aufgefallen (kein eigenes Issue angelegt):
✅ Nachgelegt: die offenen Punkte sind zu.
Nach „mach bitte alles sicher“ sind jetzt sechs der sieben Befunde behoben und die Härtungsliste weitgehend abgearbeitet.
X-Forwarded-ForumgehbarS3 — aus der Sperre wird eine Wartezeit. Meine frühere Absage bezog sich auf einen anderen Weg (während der Sperre weiter prüfen). Der richtige Weg braucht gar keine Prüfung: Die ersten vier Fehlversuche bleiben folgenlos, danach verdoppelt sich die Wartezeit bis zu einer Minute.
Der Besitzer kommt spätestens nach einer Minute hinein statt nach einer Viertelstunde gar nicht. Ausgewertet wird die Wartezeit vor der Passwortprüfung — ein ohnehin abgewiesener Versuch kostet keine 128 MiB.
S4 — drei Stufen statt einer.
X-Forwarded-Fornur noch mitTRUST_PROXY=true. Ohne Proxy wählt den Wert jeder Aufrufer selbst; eine so umgehbare Bremse ist eine Behauptung, dann lieber ehrlich keine.CSP mit Nonce. Vollständige Policy statt nur
frame-ancestors:strict-dynamic, frischer Nonce je Anfrage,default-src 'self',object-src 'none',base-uri/form-actionauf'self'. Ein eingeschleustes Skript hat keinen Nonce und läuft nicht.Zwei Lockerungen sind bewusst und stehen begründet in der Doku:
style-srcerlaubt weiter'unsafe-inline'(ein Nonce deckt keinestyle="…"-Attribute, und über eines lässt sich kein Code ausführen), undmedia-srcerlaubthttps:(der Typ „Video“ nimmt ausdrücklich Direktlinks an — das hat mir der Test auch prompt um die Ohren gehauen).Protokoll.
src/lib/auth/protokoll.tsschreibt je Ereignis eine JSON-Zeile: Fehlversuche mit Zahl und Wartezeit, abgelehnte und erfolgte OIDC-Verknüpfungen, jede Vergabe von Administratorrechten. Ohne Passwörter, Token und Adressen — Konten stehen mit ihrer Id da.Neue Einstellung:
TRUST_PROXY(Vorgabefalse) in.env.example,docker-compose.ymlund README.Geänderte Dateien (zusätzlich zum ersten Durchgang)
src/lib/auth/throttle.ts— die drei Stufensrc/lib/auth/protokoll.ts(neu)src/app/api/auth/route.ts,src/app/api/feedback/route.ts,src/app/api/auth/oidc/callback/route.tssrc/proxy.ts,src/app/layout.tsx— CSP und Nonce.env.example,docker-compose.yml,README.md,docs/sicherheit.mdSo testest du es
git switch fix/20-sicherheitspruefungdocker compose up -d --build appdocker compose logs app— die Warnung zuAUTH_ENABLED=falsesteht da.Geprüft
Aufgewendete Zeit: 01:03 (beide Durchgänge)
Pull Request: #21
Nach Freigabe:
/issue-freigabe 20🚀 Freigegeben und nach
mainübernommen.6743dc0Sechs der sieben Befunde behoben, dazu die nonce-basierte CSP und das Protokoll für Sicherheitsereignisse. Offen bleibt allein S7 — für die Prisma-Advisory gibt es noch keine Fassung ohne
deepmerge-ts@7.1.5.Typprüfung und Linter auf
mainnach dem Merge: grün.Issue wird geschlossen.