Vai al contenuto
Simone Russo
Torna alle note di sicurezza

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
01

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.

Sessione
Fattura
Build

Dopo la prima richiesta, cambiare un'opzione la riesegue con la nuova configurazione.

Traccia del server

in attesa

  1. 01Sessione
  2. 02Handler
  3. 03Lookup
  4. 04Ownersaltato
RichiestaGET /api/invoices/2

$ GET /api/invoices/2

Alice che legge la fattura 2 è già impostato. Premi «Invia richiesta» per seguire la traccia.

Risposta

—

application/json
…

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.

02

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.

  1. 01

    Prendi il codice

    Clona il repository ed entraci. Nessuna installazione: il lab è Node puro, senza dipendenze.

    Terminale
    git clone https://github.com/ErRusso/errusso.github.io.git
    cd errusso.github.io
  2. 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.

    Terminale
    node labs/idor/vulnerable.mjs

    Output

    VULNERABLE training API: http://127.0.0.1:8787
    Synthetic data only. Loopback only. Do not deploy.
  3. 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.

    Terminale
    curl -s -c /tmp/idor.cookie \
      -d 'username=alice&password=alice' \
      http://127.0.0.1:8787/login

    Output

    {"user":{"id":1,"username":"alice"}}
  4. 04

    Cambia l'id

    Sei ancora Alice. La fattura 2 appartiene a Bob (utente 2). La build vulnerabile risponde 200 con la fattura completa.

    Terminale
    curl -s -b /tmp/idor.cookie \
      http://127.0.0.1:8787/api/invoices/2

    Output

    {"id":2,"ownerId":2,"customer":"Bob Srl","amount":5400,"currency":"EUR"}
  5. 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.

    Terminale
    node 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/2

    Output

    404
  6. 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.

    Terminale
    node labs/idor/test.mjs

    Output

    [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.

03

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.

Build vulnerabile
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);
Build corretta
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.

04

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.

05

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.

01

Autenticazione

Esiste una sessione valida?

Sì — utente 1 (alice)

02

Autorizzazione

L'utente 1 può leggere la fattura 2?

Mai chiesto

06

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

Terminale
$ 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 assertions

Il test verifica anche lo stato vulnerabile. Un test che controlla solo il fix può passare perché la richiesta non ha mai funzionato davvero.

07

Checklist di review

Cinque domande per ogni id in una route

Cosa chiedo ora prima di considerare finito un endpoint.

  1. 01

    Chi può usare questo id, e dove è scritto?

  2. 02

    La query filtra già per proprietario, invece di affidarsi a una guard che qualcuno può dimenticare?

  3. 03

    Cosa succede con l'id di un secondo tenant, non solo con il mio happy path?

  4. 04

    In caso di rifiuto restituisco 404, invece di confermare che l'oggetto esiste?

  5. 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

Leggi il sorgente, non solo la dichiarazione

Torna alle note di sicurezzaSimulatore