
Findet API-Routen mit schwächerer Autorisierung als ihre Gegenstücke. CVE-2026-45316 aus dem Quellcode wiederhergestellt. Enthält die negativen Ergebnisse.
Findet API-Routen, die eine schwächere Autorisierungskontrolle als ihre Geschwisterrouten tragen.
Das Tool fand CVE-2026-45316 in Open WebUI allein aus dem Quellcode, ohne Kenntnis eines Advisories:
[MISMATCH] POST /notes/{id}/pin (notes.py:pin_note_by_id)
3/3 comparable write operations on note require has_access(write);
this route requires only has_access(read)
POST /notes/{id}/update has_access(write)
POST /notes/{id}/access/update has_access(write)
DELETE /notes/{id}/delete has_access(write)
Über 2.568 Routen in fünf Produktionscodebasen hinweg erzielte es 4 Befunde. Zwei waren echt. Dieses README erklärt beide Zahlen.
Eine große Klasse von Autorisierungsfehlern ist keine defekte Prüfung. Es ist eine fehlende oder abgeschwächte – auf einer Route, deren Geschwisterrouten alle richtig lagen.
Portainer autorisierte vier Geschwister-Template-Endpunkte, aber nicht den fünften. Signal K unterzog den HTTP-Login einem Ratenlimit, den WebSocket-Login jedoch nicht. Open WebUIs Notiz-Pin-Route veränderte eine Notiz, während sie die Leserechtigung prüfte, obwohl jede andere Notizänderung die Schreibberechtigung prüfte.
Immer dieselbe Form:
✓ ✓ ✓ ✓ ✗
Das Repository enthält die Regel bereits. Eine Route hat sie gebrochen. Man rekonstruiert also die Regel aus dem Code und meldet die Ausnahme. Keine Policy-Datei, keine Konfiguration, keine Annotationen. Der Beweisbestand sind die Geschwisterrouten selbst.
python3 enforcement_coverage.py /path/to/repo
python3 enforcement_coverage.py /path/to/repo --density
python3 enforcement_coverage.py /path/to/repo --json
Python 3.10+, keine Abhängigkeiten. Nur FastAPI.
Die Bewertungen lauten MISSING, MISMATCH, PRESERVED, UNKNOWN. UNKNOWN enthält sich und wird nie gemeldet.
Extraktion. Löst Kontrollen auf aus Depends() in Signaturen, dependencies=[] in Dekoratoren, Abhängigkeiten auf Router-Ebene, Funktionskörpern, Wrapper-Funktionen und Berechtigungsklassenlisten.
Die Extraktion aus dem Funktionskörper ist entscheidend. In Open WebUI tragen 216 von 608 Handlern die entscheidende Prüfung innerhalb der Funktion:
if user.role != 'admin' and not await AccessGrants.has_access(
user_id=user.id, resource_type='note',
resource_id=note.id, permission='write', db=db,
):
raise HTTPException(status_code=403)
Operationsklasse ergibt sich daraus, was der Handler mit der Ressource tut, nicht aus dem HTTP-Verb. POST /notes/{id}/chat ist ein POST, der eine Notiz liest.
Vokabular-Klassifikation. Zwei Werte in einer Kontrollfamilie bilden nicht notwendigerweise eine Stärkeskala:
has_access {read, write} LEVEL
ensure_flow_permission {create,delete,execute,read,write} ACTION
has_permission {features.notes, workspace.tools} SCOPE
Nur Vokabulare vom Typ LEVEL unterstützen einen Stärkevergleich. Der Vergleich von FlowAction.CREATE mit einem dominanten WRITE erzeugte in Langflow sechs False Positives, bevor dies behoben wurde.
Richtungsfilter. Es werden nur Abweichungen hin zu einer weniger restriktiven Kontrolle gemeldet. Strenger als der Präzedenzfall zu sein ist keine Schwachstelle.
Zwei echte Positivbefunde.
POST /notes/{id}/pin in Open WebUI — CVE-2026-45316.
Ein weiterer Befund ist eine Berechtigungsasymmetrie in Netflix Dispatch:
POST /{incident_id}/resources verwendet IncidentViewPermission, während sechs Geschwister-Schreiboperationen IncidentEditPermission verwenden.
IncidentViewPermission gibt für jeden nicht eingeschränkten Vorfall True zurück;
IncidentEditPermission erfordert Admin, Commander oder Reporter. Der
Handler stellt die Erstellung von Tickets und Gruppen ohne weitere Prüfung in die Warteschlange.
Nicht gemeldet, weil es keinen Ort zum Melden gibt: Das Repository wurde von Netflix am 3. September 2025 archiviert und ist schreibgeschützt, es hat keine SECURITY.md, private Schwachstellenmeldungen sind für archivierte Repos deaktiviert, und Dispatch ist im Bug-Bounty-Programm von Netflix ausdrücklich als außerhalb des Geltungsbereichs gelistet. Die Veröffentlichung hier ist der einzige verbleibende Offenlegungskanal. Der Schweregrad ist niedrig – er erfordert ein authentifiziertes Organisationsmitglied und betrifft nur nicht eingeschränkte Vorfälle – und das Projekt wird nicht mehr gepflegt.
Zwei False Positives. POST /tools/{id}/valves/user/update schreibt die eigenen Valve-Einstellungen des Benutzers und benötigt legitimerweise nur Leserechte auf dem Tool – das Mutationsziel ist eine andere Entität als das Autorisierungssubjekt. Und eine Langflow-Wissensdatenbank-Route, bei der die Geschwister-Absicherung nicht erforderlich ist.
Das ist der nützliche Teil.
Danswer hat 95,9 % Kontrollabdeckung und erzielte null Befunde bei 80,7 % UNKNOWN. Sein Vokabular ist require_permission('basic_access'), ('manage_connectors') – ein Fähigkeiten-Namensraum, keine Stärkeskala. Man kann nicht sagen, dass manage_connectors schwächer ist als read_connectors.
Was Befunde vorhersagt, ist ein ressourcenbezogener Berechtigungsaufruf mit einem geordneten Stärkeargument, wie has_access(resource, read|write). Eine von fünf Codebasen hatte das.
Fünf Idiome in fünf Repositories. Jedes erforderte Arbeit am Extractor, bevor die Analyse überhaupt laufen konnte.
In jedem Repository, unter jeder Konfiguration. Die meisten Routen gehören keiner Geschwisterfamilie von drei oder mehr mit einer konsistenten Kontrolle an.
Keiner davon war im Voraus absehbar.
Depends()Defekt 13 ist der interessante Fall. Den Tag-Empfehlungsrouten von Dispatch fehlt das CaseViewPermission, das ihre Geschwister tragen. Aber diese Berechtigung gibt für jeden nicht eingeschränkten Fall True zurück, und der Dienst prüft bereits inline visibility == restricted. Die Pfade sind äquivalent. Das zu erkennen erfordert semantische Äquivalenzanalyse, nicht Struktur.
Der Mechanismus funktioniert. Er hat aus dem Quellcode eine veröffentlichte CVE und eine nicht gemeldete Autorisierungslücke geborgen, mit prüfbaren Beweisbeständen, und nutzte nur Informationen, die vor dem Merge des verwundbaren Commits verfügbar waren.
Die Ausbeute liegt bei ungefähr einem Befund pro 1.000 Routen; das Verfahren benötigt eine Architektur, wie sie eine von fünf umfangreichen Codebasen hatte.
Nützlich als Audit-Werkzeug für eine Codebasis mit ressourcenbezogenem Berechtigungsvokabular. Nach diesen Belegen kein Allzweck-Scanner.
Die statische Erkennung von Zugriffskontrollschwachstellen durch Ableiten impliziter Annahmen geht auf die USENIX Security 2011 zurück. ACMiner hat Autorisierungsprüfungen in Android-Middleware analysiert. Semgrep liefert KI-gestützte Erkennung für fehlende Autorisierung und berichtete in einer Kundenbewertung 61 % Präzision. OWASP veröffentlicht ein Spickzettel zum Regressionstest der Autorisierung, dessen empfohlener Ansatz das manuelle Pflegen einer Actor × Resource × Action-Matrix ist.
Das ist ein enger, deterministischer Ansatz: zurückgewonnen wird nur, was Geschwisterrouten beweisen, andernfalls enthält man sich.
MIT-lizenziert.
| Repository | Routen | kontrolliert | Befunde |
|---|
| LiteLLM | 808 | 87.1% | 0 |
| Danswer / Onyx | 653 | 95.9% | 0 |
| Open WebUI | 529 | 97.2% | 2 |
| Netflix Dispatch | 291 | 36.1% | 1 |
| Langflow | 287 | 47.4% | 1 |
| Repository | Idiom |
|---|
| Open WebUI | AccessGrants.has_access(resource_type=, permission=) |
| Langflow | ensure_<resource>_permission(user, Action.X) |
| LiteLLM | Rollenvergleich anhand user_api_key_dict.user_role |
| Netflix Dispatch | Depends(PermissionsDependency([CaseEditPermission])) |
| Danswer | require_permission('basic_access') |