Codice generato da AI: perché un audit di sicurezza è oggi più urgente che mai
Il codice generato dagli assistant AI entra in produzione più in fretta di quanto venga revisionato, e questo squilibrio è il vero problema di sicurezza del 2026. Non perché l'AI scriva codice pessimo, anzi: spesso scrive codice dignitoso, pulito, idiomatico. Il problema è più sottile, ed è esattamente quello che rende il mio lavoro di chi cerca bug per mestiere così necessario in questa fase: l'AI scrive codice plausibile, e plausibile non significa sicuro. Un output che sembra corretto supera la revisione fatta a colpo d'occhio, passa il test del percorso felice, e finisce in produzione portando con sé le vulnerabilità che vivono dove l'AI non stava ottimizzando, cioè nei casi limite avversari. Auditare questo codice non è pignoleria, è la contromisura alla velocità con cui viene prodotto, di cui ho scritto nel pezzo su cosa significa che l'AI scrive gran parte del codice: più codice generato a fronte di una capacità di review invariata significa più vulnerabilità che scivolano in produzione.
Il codice generato da un'AI non fallisce dove sembra sbagliato, fallisce dove sembra giusto. La sicurezza non vive nel percorso felice che il modello ottimizza, vive nel caso limite avversario che il modello non aveva in mente, ed è lì che devi guardare.
Anthropic stessa, nei suoi dati interni, ha riportato che una review automatica retrospettiva avrebbe intercettato circa un terzo dei bug dietro a incidenti passati (lo cita nel saggio When AI builds itself): un dato da prendere con il caveat che è interno e auto-dichiarato, ma che indica un ordine di grandezza utile. Un terzo dei problemi era intercettabile con una revisione disciplinata. La domanda, allora, non è se auditare il codice AI, è come farlo in modo sistematico invece che a sensazione.
Perché il codice AI è pericoloso proprio quando sembra corretto?
La risposta sta in come questi modelli sono addestrati. Un large language model ottimizza per produrre output che assomigli a codice funzionante visto nel suo addestramento, ed è bravissimo a farlo. Ma "assomigliare a codice funzionante" e "essere sicuro" sono due proprietà diverse, e la seconda non è ciò per cui il modello è premiato. Il codice generato tende quindi a gestire benissimo il caso d'uso descritto nel prompt, il percorso che l'utente aveva in testa, e a trascurare tutto ciò che sta intorno: l'input malformato, il caso limite, l'avversario che non si comporta come l'utente medio. È esattamente l'inverso della mentalità di sicurezza, che parte dal presupposto che l'input sia ostile.
C'è poi un fattore aggravante che chiamo "fiducia mal riposta nell'output". Quando un essere umano junior scrive una query SQL concatenando stringhe, un senior che fa review la guarda con sospetto perché sa che è codice inesperto. Quando la stessa query la produce un'AI, con commenti puliti e una struttura ordinata, scatta un bias di autorevolezza: sembra il prodotto di qualcosa di competente, quindi la si revisiona con meno sospetto. È un errore cognitivo pericoloso, e l'unica difesa è trattare il codice AI con lo stesso scetticismo che riserveresti al codice di uno sconosciuto, perché in fondo è esattamente questo.
Se la tua organizzazione adotta strumenti AI per scrivere codice e vuoi impostare un processo di audit di sicurezza all'altezza della velocità di produzione, nel mio hub dedicato all'AI per la sicurezza aziendale raccolgo gli articoli con la metodologia che applico.
La checklist: i pattern ricorrenti che cerco per primi
Dopo aver auditato parecchio output di assistant AI, ho un elenco di sospetti abituali. Non è esaustivo, ma sono i pattern che intercetto con maggiore frequenza, e che consiglio di mettere in cima a qualunque revisione di codice generato.
Injection non parametrizzate. È il classico, e l'AI lo produce più spesso di quanto si creda quando il prompt è vago. Una query costruita per interpolazione di stringa invece che con prepared statement è una SQL injection in attesa:
// PERICOLOSO: input concatenato direttamente nella query
$user = $db->query("SELECT * FROM users WHERE email = '$email'");
// CORRETTO: prepared statement con binding (php.net, PDO)
$stmt = $db->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);La versione pericolosa funziona perfettamente con un'email normale, supera ogni test del percorso felice, e crolla davanti al primo ' OR 1=1 --. La regola, che il manuale ufficiale di php.net sui prepared statement ribadisce da sempre, non ammette eccezioni: nessun input dell'utente entra mai in una query per concatenazione.
Validazione mancante o ottimistica. Il codice AI tende a fidarsi dell'input. Estrae un campo da una richiesta e lo usa, assumendo che sia del tipo e del formato attesi. Manca quasi sempre la validazione del confine: cosa succede se il campo è assente, se è di tipo sbagliato, se supera la lunghezza prevista, se contiene caratteri di controllo. Ogni punto in cui un dato esterno entra nel sistema è un confine di fiducia, e il codice generato lo attraversa con disinvoltura.
Segreti hardcoded. Negli esempi, l'AI infila volentieri una chiave API, una password di database, un token, come placeholder. Il problema è che quegli esempi vengono copiati in produzione, segreto incluso, perché "funzionava". Ho visto credenziali reali finire in un repository perché erano nate come placeholder in un suggerimento e nessuno le ha sostituite. Ogni stringa che assomiglia a un segreto, in codice generato, va trattata come un allarme.
Dipendenze obsolete suggerite con fiducia. Questo è insidioso e specifico dell'AI. Il modello ha un cutoff di addestramento, e tende a suggerire versioni di librerie che conosceva allora, talvolta versioni con CVE note e patchate da tempo. Lo fa con la stessa sicurezza con cui suggerisce tutto il resto, senza alcun segnale che quella versione sia vecchia. Ogni dipendenza e ogni versione suggerita da un'AI va verificata contro lo stato attuale e contro i database di vulnerabilità, mai accettata sulla fiducia.
Default insicuri. La verifica del certificato TLS disabilitata "per far funzionare la richiesta", una configurazione CORS permissiva, un algoritmo di hashing debole scelto perché compariva spesso nel training, una gestione delle sessioni senza i flag di sicurezza. Sono scorciatoie che il modello prende per far girare l'esempio, e che diventano falle quando l'esempio diventa codice di produzione.
Logica di autorizzazione plausibile ma incompleta. È la categoria più difficile da intercettare, perché qui il codice sembra davvero corretto. Un controllo di permessi che copre il caso ovvio ma manca un percorso laterale, una verifica del proprietario di una risorsa che non considera l'accesso indiretto, un controllo fatto nel posto sbagliato del flusso. Sono i bug di logica applicativa, e richiedono di ragionare sul modello di minaccia, non solo di leggere il codice.
Il caso più subdolo: la logica di business che sembra giusta
Tra i pattern della checklist, quello su cui voglio fermarmi è l'ultimo, perché è il più pericoloso e il meno discusso. Le injection e i segreti hardcoded sono problemi noti, intercettabili anche da strumenti automatici. La logica di autorizzazione difettosa, invece, è codice che funziona: gestisce correttamente il caso per cui è stato scritto, passa i test, fa quello che il prompt chiedeva. Il difetto sta in ciò che il prompt non chiedeva e che il modello, di conseguenza, non ha considerato.
Un esempio dal genere di cose che trovo: a un assistant viene chiesto di scrivere l'endpoint che restituisce i dati di un ordine dato il suo identificativo. Il codice generato recupera l'ordine, verifica che l'utente sia autenticato, e restituisce i dati. Sembra corretto, e lo è per il caso pensato: un utente loggato che guarda il proprio ordine. Ma manca il controllo che l'ordine appartenga a quell'utente. Cambiando l'identificativo nell'URL, un utente accede agli ordini di chiunque altro: è una Insecure Direct Object Reference, una delle falle di autorizzazione più diffuse del web, e l'AI l'ha prodotta 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". Il modello ha risolto il problema enunciato, non quello reale. Questa è la trappola: il codice AI risolve sempre il problema che gli scrivi, e la sicurezza vive quasi tutta nelle precondizioni che non hai scritto perché ti sembravano ovvie. Ovvie per te, non per un sistema che genera per somiglianza.
La conseguenza metodologica è che auditare il codice AI richiede di ricostruire il modello di minaccia che il prompt non conteneva. Per ogni pezzo di logica generato mi chiedo: quali precondizioni di sicurezza il prompt dava per scontate e quindi il modello ha omesso? Chi è l'utente ostile in questo flusso, e cosa può fare che l'utente onesto non farebbe? È un lavoro che nessuno strumento automatico fa al posto tuo, perché richiede di conoscere il dominio e di pensare come un attaccante, ed è esattamente dove il valore di un audit umano resta intatto anche in un mondo in cui il codice lo scrive una macchina.
Come si audita in modo sistematico, non a sensazione
La checklist serve a sapere cosa cercare, ma il processo conta quanto la lista. La differenza tra un audit serio e un'occhiata è che il primo è avversario e strutturato. Avversario significa che non leggo il codice chiedendomi "fa quello che deve?", lo leggo chiedendomi "come lo rompo?", partendo dai confini di fiducia, i punti in cui un dato esterno entra nel sistema, e seguendo cosa succede a un input ostile da lì in poi. È la mentalità offensiva applicata alla difesa, e sui vettori specifici del codice generato da LLM ho approfondito in vettori d'attacco nel codice generato da LLM.
Strutturato significa che l'audit non dipende dall'umore o dall'attenzione del momento. Un secondo modello AI come revisore in continuous integration, istruito esplicitamente a cercare i pattern della checklist, amplia la superficie di controllo e intercetta i casi più meccanici prima che arrivino all'umano: l'ho descritto come pipeline di code review assistita da LLM. Attenzione però al paradosso, che vale come avvertimento: usare un'AI per revisionare il codice di un'altra AI non chiude il cerchio da solo, perché entrambe condividono gli stessi punti ciechi. Il revisore automatico cattura i pattern noti e libera attenzione umana per i bug di logica, che restano il dominio del senior. La responsabilità finale del rilascio non si delega a un modello: si delega il lavoro meccanico, si tiene il giudizio.
C'è infine la prova del nove, quella che la mentalità offensiva rende naturale: testare il percorso avversario, non solo quello felice. Se il codice gestisce un'email, provo un'email con una virgoletta, con un payload di injection, con una stringa lunghissima, con un valore nullo. Se gestisce un'autorizzazione, provo ad accedere alla risorsa di un altro, per via diretta e indiretta. È in questi test che il codice plausibile rivela di essere insicuro, ed è la ragione per cui un audit fatto bene trova quello che una lettura, per quanto attenta, lascia passare.
Un'avvertenza pratica, perché auditare tutto con la stessa intensità è irrealistico e porta a non auditare niente: l'audit va prioritizzato per rischio. Non tutto il codice generato è uguale. Il codice che tocca i confini di fiducia, autenticazione, autorizzazione, gestione dell'input esterno, accesso ai dati, interazione con sistemi di pagamento, merita la revisione avversaria completa, perché è lì che un difetto diventa un incidente. Il codice interno, senza esposizione a input ostile e senza accesso a dati sensibili, può accontentarsi di un controllo più leggero. Sprecare l'attenzione umana, che è la risorsa scarsa, su codice a basso rischio significa toglierla a dove serve davvero. La mappa del rischio, quali parti del sistema sono esposte e a cosa, è il prerequisito di un audit efficiente, e va fatta una volta a livello di architettura, non riscoperta a ogni pull request.
Il quadro che porto a casa, da chi i bug li cerca per mestiere, è che l'AI non ha creato vulnerabilità nuove: SQL injection, validazione mancante, segreti esposti sono vecchi come il web. Ha però cambiato la scala e la velocità con cui entrano in produzione, e ha aggiunto il bias di autorevolezza che abbassa la guardia di chi revisiona. La risposta non è rinunciare agli strumenti AI, che fanno risparmiare tempo reale, è abbinarli a una disciplina di audit altrettanto seria: una checklist dei pattern ricorrenti, un processo avversario e strutturato, e la consapevolezza che il codice generato va trattato con lo scetticismo che riserveresti a uno sconosciuto. Se vuoi un audit di sicurezza del codice che entra in produzione nella tua organizzazione, generato da AI o meno, condotto con la mentalità di chi quel codice lo attacca, 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.