const invoice = INVOICES.find((item) => item.id === Number(match[1])); if (!invoice) return sendJson(response, 404, { error: "not found" }); // ! no owner check: any session can read this objectreturn sendJson(response, 200, invoice);Sicurezza / training lab
Accedi come Alice. Leggi la fattura di Bob.
Una piccola API di fatture che gira sulla tua macchina. Il login funziona. Le sessioni funzionano. La lookup funziona. Manca solo il controllo che decide se quella fattura è tua. Guardalo qui sotto, poi riproducilo davvero in due terminali.
- Solo 127.0.0.1
- Dati sintetici
- CWE-639
- OWASP API1:2023
Spiegazione interattiva
Il controllo che non c'è
La stessa logica del server del lab, disegnata come una richiesta. Scegli una sessione e una fattura, poi inviala. Passa alla build corretta e invia la stessa identica richiesta.
Dopo la prima richiesta, cambiare un'opzione la riesegue con la nuova configurazione.
Traccia del server
in attesa
- 01Sessione
- 02Handler
- 03Lookup
- 04Ownersaltato
GET /api/invoices/2$ GET /api/invoices/2
Alice che legge la fattura 2 è già impostato. Premi «Invia richiesta» per seguire la traccia.
—
…Alice che legge la fattura 2 è già impostato. Premi «Invia richiesta» per seguire la traccia.
Simulazione lato client: rispecchia la logica di labs/idor/app.mjs, non esegue il server.
Parte pratica
Due terminali, sei passi
Un terminale tiene vivo il server; l'altro invia le richieste. Ogni comando è copia-incolla e i dati sono finti. Il giro completo richiede circa due minuti.
- 01
Prendi il codice
Clona il repository ed entraci. Nessuna installazione: il lab è Node puro, senza dipendenze.
Terminalegit clone https://github.com/ErRusso/errusso.github.io.git cd errusso.github.io - 02
Avvia la build vulnerabile
Lasciala attiva nel primo terminale. Il server stampa l'URL di loopback e non si lega mai a un'interfaccia pubblica.
Terminalenode labs/idor/vulnerable.mjsOutput
VULNERABLE training API: http://127.0.0.1:8787 Synthetic data only. Loopback only. Do not deploy. - 03
Accedi come Alice
Il cookie di sessione viene scritto in /tmp/idor.cookie. Alice è l'utente 1; le sue fatture sono la 1 e la 3.
Terminalecurl -s -c /tmp/idor.cookie \ -d 'username=alice&password=alice' \ http://127.0.0.1:8787/loginOutput
{"user":{"id":1,"username":"alice"}} - 04
Cambia l'id
Sei ancora Alice. La fattura 2 appartiene a Bob (utente 2). La build vulnerabile risponde 200 con la fattura completa.
Terminalecurl -s -b /tmp/idor.cookie \ http://127.0.0.1:8787/api/invoices/2Output
{"id":2,"ownerId":2,"customer":"Bob Srl","amount":5400,"currency":"EUR"} - 05
Passa alla build corretta
Ferma il server con Ctrl-C, avvia la build corretta e ripeti la stessa richiesta con lo stesso cookie. Ora risponde 404: l'oggetto viene nascosto, non confermato.
Terminalenode labs/idor/fixed.mjs curl -s -o /dev/null -w '%{http_code}\n' \ -b /tmp/idor.cookie \ http://127.0.0.1:8787/api/invoices/2Output
404 - 06
Esegui il test di regressione
Un solo comando avvia entrambe le build e verifica la fuga, il blocco, l'accesso del proprietario e il 401 anonimo — in entrambe le direzioni.
Terminalenode labs/idor/test.mjsOutput
[vulnerable] alice -> invoice #2: 200 (Bob Srl) [fixed] alice -> invoice #2: 404 [fixed] alice -> invoice #1: 200 [fixed] anonymous -> invoice #1: 401 [fixed] bob -> invoice #2: 200 PASS: 9 assertions
Se curl non restituisce nulla, probabilmente il server non è attivo nell'altro terminale. La porta è fissa a 8787; cambiala con PORT=8788 node labs/idor/vulnerable.mjs e aggiorna l'URL.
Il bug
Autenticazione non è autorizzazione.
La route dimostra che esiste una sessione, legge l'id, carica la fattura e la restituisce. Ogni riga è corretta tranne quella assente: il confronto tra il proprietario della fattura e l'utente della sessione.
const invoice = INVOICES.find((item) => item.id === Number(match[1])); if (!invoice) return sendJson(response, 404, { error: "not found" }); // + the check that was missingif (invoice.ownerId !== session.userId) { return sendJson(response, 404, { error: "not found" });} return sendJson(response, 200, invoice);La risposta è un 200 valido con una sessione valida. Il chiamante semplicemente non ha alcuna relazione con l'oggetto. Questa è broken object-level authorization, o IDOR.
Impatto
Un numero cambiato
Privilege escalation orizzontale: qualsiasi utente autenticato può leggere la fattura di un altro tenant scorrendo un intervallo di id prevedibile.
Riservatezza: nome cliente e importo di un altro tenant.
Enumerazione: gli id 1, 2, 3… si percorrono banalmente; il test di regressione fa esattamente questo.
Una UI che nasconde il link non è una difesa: l'API risponde anche quando nessuna schermata punta all'oggetto.
Silenzioso per design: la build vulnerabile registra una richiesta riuscita, non un attacco.
Root cause
Dove va il controllo
Il login risponde a chi sta chiamando. Non può rispondere a cosa può leggere.
Il controllo di proprietà va accanto alla lookup, nel percorso di accesso ai dati, così ogni route che carica una fattura lo eredita. Una guard nella UI o un controllo di ruolo più a monte non sa che la fattura 2 appartiene all'utente 2.
Autenticazione
Esiste una sessione valida?
Sì — utente 1 (alice)
Autorizzazione
L'utente 1 può leggere la fattura 2?
Mai chiesto
Prova
Fai fallire il fix ad alta voce
Il test di regressione avvia entrambe le build e verifica cinque comportamenti: la fuga, il blocco e le tre cose che il fix non deve rompere.
Build vulnerabile: Alice legge la fattura di Bob (200).
Build corretta: la stessa richiesta viene nascosta (404).
Build corretta: Alice legge ancora la propria fattura (200).
Build corretta: l'accesso anonimo viene rifiutato (401).
Build corretta: Bob legge ancora la propria fattura (200).
$ node labs/idor/test.mjs[vulnerable] alice -> invoice #2: 200 (Bob Srl)
[fixed] alice -> invoice #2: 404
[fixed] alice -> invoice #1: 200
[fixed] anonymous -> invoice #1: 401
[fixed] bob -> invoice #2: 200
PASS: 9 assertionsIl test verifica anche lo stato vulnerabile. Un test che controlla solo il fix può passare perché la richiesta non ha mai funzionato davvero.
Checklist di review
Cinque domande per ogni id in una route
Cosa chiedo ora prima di considerare finito un endpoint.
- 01
Chi può usare questo id, e dove è scritto?
- 02
La query filtra già per proprietario, invece di affidarsi a una guard che qualcuno può dimenticare?
- 03
Cosa succede con l'id di un secondo tenant, non solo con il mio happy path?
- 04
In caso di rifiuto restituisco 404, invece di confermare che l'oggetto esiste?
- 05
Esiste un test che fallisce se qualcuno rimuove il controllo di proprietà?
Perimetro e onestà
Cosa non è questa pagina
Questo è un training lab, non un penetration test retribuito e non un CVE reale. L'app è deliberatamente vulnerabile, ascolta solo su loopback e usa dati sintetici. Dimostra metodo e remediation, non impatto in produzione. Il target l'ho costruito io, quindi lo sfruttamento è autorizzato.
Riferimenti