È sotto attacco? Richieda supporto per la risposta agli incidenti

[email protected]
Sicurezza web

Testare ogni ID della propria app contro IDOR e BOLA

Come trovare gli endpoint che restituiscono dati altrui cambiando un ID, correggerli con ricerche vincolate all'utente e cosa verificare in code review.

Anche in:EnglishTürkçeDeutschFrançaisEspañolPolskiNederlandsEesti

Illustrazione: Testare ogni ID della propria app contro IDOR e BOLA

Una insecure direct object reference (IDOR) esiste ovunque la Sua applicazione accetti un identificatore dal client e restituisca o modifichi il record corrispondente senza chiedersi se quell'utente abbia il diritto di accedervi. Nel mondo delle API la stessa falla si chiama broken object-level authorisation (BOLA) e occupa il primo posto nella classifica OWASP API Security 2023. Per prevenirla serve un controllo di autorizzazione a ogni accesso a un oggetto, applicato a tutta l'applicazione e coperto da test.

Da cosa è composto un IDOR e dove si nasconde

Il cheat sheet OWASP sugli IDOR scompone un IDOR in tre parti: un oggetto (un account, un documento, un ticket, una transazione), un riferimento a esso (un ID, un UUID, un numero di conto, un token o uno slug) e un controllo a livello di oggetto che manca. La terza parte è la falla.

Il riferimento non è sempre /users/123 nell'URL. Può trovarsi in un campo nascosto di un form come user_id, in una query string, in un header della richiesta o in un payload JSON. Può anche essere un nome di file: se GET /documents/annual-report.pdf funziona, provi con il nome di un altro documento valido.

La voce OWASP API Security dedicata alla BOLA valuta lo sfruttamento come facile, la diffusione come ampia e la rilevabilità come facile. I server API di norma non tengono traccia dello stato del client, quindi si affidano agli ID inviati dal client per decidere quale oggetto caricare. Ecco gli scenari di esempio:

  • Un endpoint di analytics per negozi della forma /shops/{shopName}/revenue_data.json, combinato con un secondo endpoint che elenca tutti i nomi dei negozi, espone i dati di vendita di migliaia di punti vendita a un semplice script.
  • Un'API per veicoli che accetta un VIN ma non verifica mai che il veicolo appartenga all'utente autenticato.
  • Una mutation GraphQL deleteReports che elimina qualunque ID di documento riceva.

L'ultimo caso è una cancellazione: aggiornamenti ed eliminazioni vanno quindi testati con la stessa cura della pagina che mostra il record.

Testare con due account

Crei User A e User B, assegni a ciascuno i propri oggetti (documenti, ticket, fatture), si autentichi come A e riproduca le richieste con gli identificatori di B.

  1. Crei due account con ambiti di autorizzazione diversi.
  2. Crei oggetti di proprietà di ciascun utente.
  3. Acceda come User A e catturi ogni richiesta che contiene un riferimento a un oggetto.
  4. Sostituisca i riferimenti di User B nel path, nella query, nel body e negli header.
  5. Verifichi che l'applicazione neghi l'accesso ogni volta.

Copra lettura, creazione, modifica, cancellazione, esportazione e azioni amministrative. Non salti un endpoint solo perché i suoi identificatori sembrano casuali. Il cheat sheet afferma che l'accesso deve essere negato sia che l'identificatore sia prevedibile, sia che non sia indovinabile.

Trasformarlo in un test di regressione

Quando la verifica manuale funziona, la trasformi in codice. Ecco uno schema in pytest, che presuppone fixture in grado di fornire una sessione autenticata per User A e l'ID di un documento di proprietà di B. Ogni richiesta deve essere per il resto valida, con un body ben formato, in modo che un 400 o un 415 dovuto alla validazione dell'input non possa far superare il test per il motivo sbagliato:

import os
import pytest

BASE_URL = os.environ["APP_BASE_URL"]  # from your test config
DENIED = 404  # your chosen response for foreign IDs

ENDPOINTS = [
    ("GET", "/documents/{id}", None),
    ("PUT", "/documents/{id}", {"title": "changed"}),
    ("DELETE", "/documents/{id}", None),
    ("GET", "/documents/{id}/export", None),
    ("POST", "/documents/{id}/comments", {"text": "hello"}),
]

@pytest.mark.parametrize("method,path,body", ENDPOINTS)
def test_a_cannot_touch_bs_document(method, path, body, session_a, b_document_id):
    url = BASE_URL + path.format(id=b_document_id)
    r = session_a.request(method, url, json=body)
    assert r.status_code == DENIED

Scelga con consapevolezza tra 403 e 404. Un 403 per un ID altrui conferma che l'ID esiste, e questo aiuta un attaccante a enumerare. Qualunque codice scelga, verifichi con assert quell'unico codice. Generi l'elenco degli endpoint dalla tabella delle route, così i nuovi endpoint compaiono automaticamente, e blocchi i deploy quando la suite fallisce.

GraphQL richiede due casi in più. Interroghi un ID altrui a ogni livello dell'albero per verificare se i resolver annidati ripetono il controllo del genitore. Per le operazioni batch, come l'array reportKeys citato sopra, invii una richiesta che mescola ID propri e altrui e verifichi che l'intera chiamata fallisca oppure che l'elemento altrui venga saltato.

Limitare la ricerca all'utente

Controlli l'autorizzazione per ogni oggetto a cui un utente tenta di accedere, usando il meccanismo offerto dal Suo framework, in modo che il controllo sia strutturale e non qualcosa che ogni sviluppatore deve ricordarsi di fare. Il pattern più diretto consiste nel cercare gli oggetti all'interno dell'insieme raggiungibile dall'utente corrente. Ecco la versione per Rails:

# vulnerable, searches all projects
@project = Project.find(params[:id])

# secure, searches projects related to the current user
@project = @current_user.projects.find(params[:id])

L'handler Spring Boot nel cheat sheet chiama findById(id) e quindi restituisce qualunque documento a chiunque riesca a indovinare o ottenere un ID. Il frammento seguente è un nostro esempio, non codice di OWASP. Presuppone una derived query di Spring Data su un campo ownerId, mentre currentUser() rappresenta il Suo helper che legge il contesto di autenticazione attendibile:

@GetMapping("/documents/{id}")
public Document getDocument(@PathVariable Long id) {
    return documentRepository
        .findByIdAndOwnerId(id, currentUser().getId())
        .orElseThrow();
}

Dove possibile, eviti di esporre gli identificatori e ricavi l'utente dalla sessione. Nei flussi a più passaggi, conservi gli identificatori nella sessione, così che il client non possa manometterli tra un passaggio e l'altro.

Gli identificatori casuali aiutano, ma solo come difesa in profondità. Aggiunga una colonna con una stringa casuale e la usi negli URL, oppure usi gli UUID come chiavi primarie. Non cifri gli ID per nasconderli: farlo in modo sicuro è difficile.

Cosa rientra nella code review

Legga ogni handler che riceve un ID fornito dal client e individui dove avviene il controllo dei permessi. Verifichi anzitutto se la query viene eseguita su un insieme vincolato all'utente oppure chiama un find globale e controlla dopo, ammesso che lo faccia. Poi si assicuri che il controllo esista in ogni funzione che raggiunge un record a partire da un input del client, compresi aggiornamento, cancellazione ed esportazione.

Faccia attenzione a un handler che si limita a confrontare l'ID dell'utente di sessione (ad esempio estratto dal JWT, che può ispezionare con il nostro strumento JWT) con il parametro ID. Secondo OWASP questo copre solo un piccolo sottoinsieme di casi. L'autorizzazione deve seguire le policy e la gerarchia degli utenti. In un sistema multi-tenant, un admin del tenant X può richiedere una fattura che appartiene al tenant Y. L'utente è autenticato, il ruolo è admin e l'ID è valido, quindi un semplice controllo sul ruolo può essere superato mentre l'oggetto si trova nel tenant sbagliato.

Nel triage tenga distinte due falle. Nella BOLA l'utente è autorizzato per progetto a chiamare l'endpoint e la violazione è l'ID manipolato. Se un utente raggiunge un endpoint che non dovrebbe raggiungere affatto, si tratta di broken function-level authorisation, che richiede una correzione diversa.

Da dove iniziare

  1. Faccia un inventario di ogni endpoint, form e operazione GraphQL che accetta un identificatore, compresi nomi di file e campi nascosti. Parta dalla Sua specifica OpenAPI o dalla tabella delle route, poi aggiunga ciò che emerge solo nella cronologia del proxy mentre naviga nell'applicazione.
  2. Esegua la verifica con due utenti descritta sopra, iniziando dai dieci oggetti più sensibili, come fatture, dati personali e azioni amministrative, e registri ogni risposta che non sia un diniego.
  3. Sostituisca le ricerche globali con ricerche vincolate all'utente, usando il meccanismo di autorizzazione del Suo framework.
  4. Inserisca nella pipeline i test parametrizzati tra utenti diversi e faccia in modo che un fallimento blocchi il rilascio.
  5. Aggiunga le domande di review al template delle pull request. Per una verifica esterna di ciò che i Suoi test hanno mancato, può commissionare il nostro servizio di penetration testing.

Come possiamo aiutarla

Penetration test

Attacchiamo reti, applicazioni e persone come farebbe un avversario reale, poi le mostriamo come chiudere ogni falla.

Decoder JWT

Decodifica un JSON Web Token e cerca gli errori di sicurezza più comuni, senza che lasci il browser.

Ci scriva
Team AngelForce

Scritto dal team AngelForce: consulenti di sicurezza con certificazioni CEH, LPT Master e CISA che testano, monitorano e difendono i sistemi di organizzazioni in tutta Europa e in Turchia.

Scritto con l’assistenza dell’intelligenza artificiale a partire dalle fonti pubbliche elencate di seguito e rivisto dal team di AngelForce prima della pubblicazione.

Fonti

  1. cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html
  2. api-security.owasp.org/editions/2023/en/0xa1-broken-object-level-authorization/

Articoli: Sicurezza web

Ci dica cosa deve proteggere

Descriva il suo ambiente e cosa vuole ottenere. Risponderemo via email.

Ci scriva
[email protected]

AngelForce OÜ · Tallinn, Estonia