I vettori d'attacco del codice generato da LLM: analisi offensiva delle vulnerabilità tipiche
In un altro articolo ho affrontato dal lato difensivo i pattern di vulnerabilità ricorrenti nel codice generato da AI, con la checklist di audit di sicurezza del codice AI. Qui ribalto il punto di vista e scendo più in profondità: guardo lo stesso codice con gli occhi di chi lo attacca per mestiere, e mi concentro su quattro classi di vulnerabilità che gli LLM introducono in modo prevedibile. La prevedibilità è la chiave, ed è anche la cattiva notizia per chi sviluppa con assistenza AI senza saperlo: un modello genera il pattern più comune che ha visto nei dati di addestramento, e il pattern più comune sul web, per moltissimi problemi, è anche quello insicuro. Un attaccante esperto lo sa, e parte avvantaggiato, perché sa già cosa l'AI tende a produrre prima ancora di leggere il tuo codice.
Il codice generato da un LLM è il pattern medio del suo addestramento. Il pattern medio del web, su deserializzazione, fetch remoti, autorizzazione e concorrenza, è il pattern insicuro. L'attaccante non indovina dove guardare: scommette su ciò che la macchina, statisticamente, ha scritto.
Le quattro classi che analizzo sono deserializzazione insicura, SSRF, bypass dell'autorizzazione e race condition. Non sono le uniche, ma sono quelle in cui la prevedibilità dell'output AI si traduce più direttamente in un exploit, e quelle che cerco per prime quando audito codice scritto con assistenza.
Perché un attaccante sa già dove guardare nel codice AI?
La risposta sta nella natura statistica del modello. Quando chiedi a un assistant di deserializzare un dato, di scaricare una risorsa da un URL fornito dall'utente, di controllare se un utente può vedere una risorsa, o di aggiornare un saldo, il modello produce la soluzione modale: quella che ha visto più spesso nei tutorial, negli esempi, nel codice pubblico su cui è stato addestrato. E quel corpus è dominato da esempi didattici che ottimizzano per la brevità e la chiarezza, non per la sicurezza. Il tutorial medio su come deserializzare un oggetto usa unserialize sull'input grezzo; l'esempio medio di "scarica questa immagine da un URL" non valida la destinazione; il frammento medio di controllo accessi verifica che l'utente sia loggato, non che la risorsa sia sua.
Per l'attaccante, questo trasforma l'audit da una caccia esplorativa a una verifica mirata. Non deve mappare l'intera applicazione cercando l'anomalia: prende le quattro classi note, va dritto ai punti dell'applicazione dove quelle operazioni avvengono, e controlla se il codice ha la forma insicura prevista. È un enorme vantaggio di efficienza, ed è il motivo per cui difendersi richiede di ribaltare la stessa predittività a proprio favore: se sai cosa l'AI tende a produrre, sai anche dove guardare per primo nella tua difesa.
Se la tua organizzazione sviluppa con assistenza AI e vuoi una valutazione di sicurezza condotta con la mentalità di chi cerca questi vettori per mestiere, nel mio hub dedicato all'AI per la sicurezza aziendale raccolgo gli articoli con la metodologia che applico.
Deserializzazione insicura: dall'input grezzo alla RCE
È la più grave, perché può portare direttamente all'esecuzione di codice. Il pattern che l'AI produce quando gli chiedi di ricostruire un oggetto da un dato serializzato è quasi sempre la forma diretta e pericolosa:
// PERICOLOSO: deserializzazione di input non fidato
$obj = unserialize($_COOKIE['session_blob']);Dal lato attaccante, questo è un invito. Se l'applicazione deserializza un dato che io controllo, e nella codebase o nelle sue dipendenze esiste una classe con i metodi giusti (le cosiddette gadget chain, che sfruttano metodi magici come __destruct o __wakeup), posso costruire un payload che, una volta deserializzato, esegue azioni non previste: scrivere file, invocare comandi, in alcuni casi ottenere esecuzione di codice remoto. La stessa classe di problema esiste fuori da PHP, nei formati di serializzazione di altri linguaggi, ed è alla radice di incidenti reali sulle pipeline AI, come nel caso della RCE via pickle in LangGraph: cambia il linguaggio, non il principio.
La remediation è netta e non ammette mezze misure: non deserializzare mai dati non fidati con i meccanismi nativi degli oggetti. Per i dati che attraversano un confine di fiducia si usa un formato dati, non un formato oggetti, cioè JSON. Quando la deserializzazione di oggetti è inevitabile, il manuale di php.net su unserialize documenta l'opzione allowed_classes per limitare quali classi possono essere istanziate, che riduce drasticamente la superficie delle gadget chain. L'AI questa difesa non la mette quasi mai da sola, perché il pattern modale del training non la include.
SSRF: il fetch ingenuo che bussa alla rete interna
Quando chiedi a un assistant di costruire una funzionalità che scarica una risorsa da un URL fornito dall'utente, anteprime di link, importazione di immagini, webhook, il codice generato fa esattamente ciò che hai chiesto: scarica l'URL. Quello che non fa è chiedersi quale URL. Ed è la radice della Server-Side Request Forgery:
// PERICOLOSO: l'URL viene dall'utente, nessuna validazione della destinazione
$preview = file_get_contents($_POST['image_url']);Dal lato offensivo, un endpoint del genere è una finestra sulla rete interna del server. Non fornisco l'URL di un'immagine: fornisco http://169.254.169.254/, l'endpoint dei metadati che su molti ambienti cloud espone le credenziali dell'istanza, oppure l'indirizzo di un servizio interno non esposto a internet. Il server, fidandosi, va a bussare per conto mio dove io non potrei arrivare, e mi restituisce la risposta. È uno dei vettori con cui un'esfiltrazione di credenziali cloud comincia, e il codice AI lo produce con disarmante naturalezza perché "scarica l'URL" è esattamente ciò che gli è stato chiesto.
La difesa è una validazione rigorosa della destinazione: una allowlist di domini o di schemi consentiti dove possibile, il blocco esplicito degli intervalli di indirizzi privati e link-local (incluso il range dei metadati cloud), e il divieto di seguire redirect verso destinazioni interne. È un controllo che l'AI ometterà sistematicamente, perché nei suoi esempi di training il fetch è quasi sempre nudo.
Bypass dell'autorizzazione: il controllo che c'è ma non basta
Questo è il più subdolo, perché il codice sembra sicuro: un controllo di autorizzazione c'è. Solo che è quello sbagliato. Il pattern tipico che l'AI genera quando gli chiedi un endpoint che restituisce una risorsa è verificare che l'utente sia autenticato, e fermarsi lì:
// PERICOLOSO: verifica che sia loggato, NON che la risorsa sia sua
if (!$user->isAuthenticated()) { abort(401); }
return Order::find($_GET['id']);Dal lato attaccante, questa è una Insecure Direct Object Reference, una delle falle più diffuse e più facili da sfruttare: sono loggato come me stesso, perfettamente autenticato, e cambio l'identificativo nell'URL per leggere l'ordine di un altro, la fattura di un altro, i dati di un altro. Non ho bypassato l'autenticazione, ho sfruttato l'assenza del controllo di proprietà. L'AI produce questo difetto non per incompetenza ma perché il prompt diceva "restituisci l'ordine dato l'id", non "restituisci l'ordine dato l'id se appartiene all'utente": ha risolto il problema enunciato, e la sicurezza viveva nella precondizione non scritta. È la stessa dinamica di composizione dei permessi che ho analizzato per gli agenti in MCP e over-broad token scoping: il privilegio effettivo non è quello che pensi di aver concesso.
La remediation è strutturale: l'autorizzazione va ancorata alla proprietà del dato, idealmente a livello di query (recuperare l'ordine vincolandolo all'utente corrente, non recuperarlo e poi sperare), e va verificata su ogni accesso indiretto, non solo su quello diretto.
Race condition: il check-then-act senza atomicità
L'ultima classe è quella che il codice AI sbaglia perché ragiona, come quasi tutti gli esempi didattici, in modo sequenziale e single-thread. Quando chiedi di applicare un buono sconto una sola volta, di non scendere sotto zero su un saldo, di non assegnare due volte la stessa risorsa, il pattern generato è il classico controlla-poi-agisci:
// PERICOLOSO: tra il controllo e l'azione c'e' una finestra di corsa
if ($coupon->uses < $coupon->max_uses) {
$coupon->uses++; // due richieste concorrenti passano entrambe il check
$coupon->save();
}Dal lato offensivo, questo è un Time-Of-Check to Time-Of-Use, e lo sfrutto con la concorrenza: invio molte richieste nello stesso istante, e tutte superano il controllo prima che una qualsiasi aggiorni lo stato. Il risultato è il buono usato cento volte, il saldo che va sotto zero, la risorsa assegnata due volte: nel mondo reale è frode, doppia spesa, perdita economica diretta. L'AI non vede questa finestra perché nei suoi esempi le richieste arrivano una alla volta, e la concorrenza è proprio ciò che gli esempi didattici astraggono via.
La difesa è rendere l'operazione atomica: un vincolo a livello di database che impedisca lo stato invalido, un aggiornamento condizionale in una sola istruzione (decrementare verificando nello stesso comando), o un lock appropriato. Non è codice difficile, è codice che richiede di pensare alla concorrenza, e pensare alla concorrenza è esattamente ciò che il pattern modale dell'AI non fa.
Perché proprio queste quattro classi
Non è un elenco casuale, e capire cosa le accomuna aiuta a riconoscerne altre. Tutte e quattro condividono una proprietà precisa: sono vulnerabilità in cui la distanza tra "il codice sembra corretto" e "il codice è sicuro" è massima. La deserializzazione insicura funziona: ricostruisce l'oggetto. Il fetch SSRF funziona: scarica la risorsa. Il controllo di autorizzazione incompleto funziona: nega l'accesso a chi non è loggato. Il check-then-act funziona: in un test sequenziale, applica il buono una volta sola. In tutti e quattro i casi il codice passa il percorso felice, supera i test che un umano scriverebbe per il caso d'uso descritto, e va in produzione con l'aria di funzionare. Il difetto vive in una dimensione che il test ovvio non esplora: l'input ostile, la destinazione non prevista, l'accesso indiretto, la concorrenza.
È esattamente questa la ragione per cui sono il terreno di caccia preferito di chi attacca e l'incubo di chi revisiona in fretta. Una SQL injection scritta per concatenazione la vede anche uno strumento automatico; un IDOR no, perché non c'è nulla di sintatticamente sbagliato da segnalare. Sono falle di logica e di modello di minaccia, non di sintassi, e gli strumenti automatici che ragionano per pattern le mancano sistematicamente. Servono occhi che ragionano come un attaccante, ed è il motivo per cui un audit umano competente resta insostituibile anche, e soprattutto, in un mondo in cui il codice lo scrive una macchina.
C'è poi un fattore che amplifica tutto, ed è la velocità. Il codice generato entra in produzione più in fretta di quanto venga revisionato: più volume a fronte di una capacità di review invariata significa, statisticamente, più di queste falle che scivolano oltre il controllo. E si aggiunge il bias di autorevolezza: il codice AI è pulito, ordinato, commentato bene, e questo abbassa la guardia di chi lo revisiona, che gli concede una fiducia che non concederebbe al codice di uno sconosciuto, mentre in fondo è esattamente questo. La combinazione, più codice e meno sospetto, è il moltiplicatore che trasforma quattro classi di vulnerabilità note in un problema di scala nuovo. Non perché l'AI inventi falle inedite, ma perché ne produce di vecchie più in fretta e con un travestimento di competenza che le fa passare.
Ribaltare la prevedibilità a tuo favore
Il filo che lega le quattro classi è che la debolezza dell'AI è anche la tua arma difensiva. Un attaccante esperto è efficiente perché sa cosa cercare; ma se lo sai anche tu, parti dallo stesso vantaggio. Auditare codice generato non significa rileggere tutto sperando di notare qualcosa: significa andare dritti ai punti dove avvengono deserializzazione, fetch remoti, controlli di autorizzazione e operazioni concorrenti, e verificare se hanno la forma insicura prevedibile. La prevedibilità che avvantaggia chi attacca è la stessa che rende sistematica la tua difesa. In pratica, quando rivedo un diff generato con assistenza AI, la prima passata non è una lettura lineare: è una ricerca mirata. Cerco ogni deserializzazione e mi chiedo da dove viene il dato; cerco ogni chiamata di rete e mi chiedo chi sceglie la destinazione; cerco ogni recupero di risorsa per identificativo e mi chiedo dov'è il controllo di proprietà; cerco ogni operazione che legge-poi-scrive uno stato condiviso e mi chiedo cosa succede con due richieste in parallelo. Quattro domande, applicate ai quattro punti dove l'AI inciampa, intercettano la stragrande maggioranza dei problemi prima ancora di entrare nel merito della logica. È il principio che applico sempre: non puoi difendere ciò che non hai pensato di attaccare, e con il codice AI sai già da dove un attaccante comincerebbe. Se vuoi un'analisi offensiva del codice che la tua organizzazione porta in produzione, generato da AI o meno, condotta cercando esattamente questi vettori, puoi usare il modulo di preventivo gratuito: sette domande, due minuti, e ti dico se il tuo caso rientra nel mio perimetro o se ti conviene un'altra figura.