Sicurezza del codice generato da AI: audit sistematico di output ChatGPT e Copilot in PHP
Tra gennaio e settembre 2025, durante gli audit di sicurezza che conduco su applicazioni PHP per clienti PMI, ho iniziato a notare un pattern nuovo: vulnerabilità con una "firma" riconoscibile che non corrispondeva allo stile di scrittura degli sviluppatori del team. Query SQL con concatenazione di stringhe in un progetto che usava Eloquent ovunque. Validazione dell'input assente su endpoint dove il rest del codice aveva FormRequest rigorosi. Gestione degli errori con die() in un contesto dove il progetto usava exception handler Laravel. Quando ho chiesto ai team, la risposta era la stessa: "Quel pezzo l'ha scritto Copilot" o "L'ho generato con ChatGPT e funzionava, quindi l'ho committato."
Ho deciso di quantificare il problema. Ho raccolto 200 snippet PHP generati da GitHub Copilot e ChatGPT nel contesto di 8 progetti clienti, utilizzati dagli sviluppatori nei 9 mesi precedenti e committati nel repository senza modifiche significative. Li ho sottoposti allo stesso processo di audit che uso per il codice scritto da umani: analisi statica con PHPStan, review manuale per vulnerabilità OWASP, e test di exploitation dove possibile. I risultati: il 23% degli snippet conteneva almeno una vulnerabilità, di cui l'8% classificabile come high severity (SQL injection, XSS stored, path traversal). Il 77% era corretto dal punto di vista della sicurezza - il che significa che l'AI produce codice sicuro nella maggioranza dei casi, ma con un tasso di errore del 23% che è troppo alto per accettare output AI senza review.
Quali vulnerabilità produce più frequentemente il codice generato da AI?
I pattern di vulnerabilità nel codice AI-generated sono diversi dai pattern nel codice scritto interamente da umani - e questa differenza è il motivo per cui gli strumenti di audit tradizionali (che sono calibrati sugli errori umani) non sono sufficienti per catturare i bug dell'AI. Gli errori umani tipici sono errori di omissione (dimenticare di validare un campo, dimenticare di fare escape dell'output) in codice che è strutturalmente corretto. Gli errori AI sono spesso errori di stile - il codice AI replica pattern obsoleti o insicuri che ha visto nel training data, producendo codice che "sembra funzionare" ma che usa tecniche deprecate o pericolose.
Le cinque vulnerabilità più frequenti nei 200 snippet analizzati sono state, in ordine di frequenza:
1. SQL injection da concatenazione di stringhe (12% degli snippet con query). L'AI genera frequentemente query come $db->query("SELECT * FROM users WHERE email = '$email'") invece di usare prepared statement. Il motivo è che il training data contiene milioni di esempi di codice PHP con concatenazione di stringhe - un pattern comune nei tutorial pre-2010 e nelle risposte Stack Overflow vecchie - e il modello tende a replicare il pattern più frequente nel training, non il pattern più sicuro. In un progetto Laravel dove tutto il team usa Eloquent, uno snippet Copilot che introduce una query raw con concatenazione è particolarmente insidioso perché si nasconde in mezzo a codice che è tutto corretto.
2. Assenza di validazione dell'input (18% degli snippet con parametri utente). L'AI genera endpoint che accettano parametri dall'utente e li usano direttamente senza validazione: $id = $request->input('id'); $user = User::find($id); senza verificare che $id sia un intero positivo. In isolamento, questo snippet non è vulnerabile (Eloquent usa prepared statement per il find()), ma stabilisce un pattern pericoloso: i parametri utente vengono usati trust-first, e quando il prossimo snippet usa lo stesso parametro in un contesto meno protetto (una query raw, un path filesystem, un comando shell), la vulnerabilità diventa exploitable.
3. Gestione degli errori con die() e informazioni di debug (9% degli snippet). L'AI genera codice con die("Error: " . $e->getMessage()) o echo $e->getTraceAsString() - pattern che espongono informazioni interne (path del filesystem, nomi delle classi, dettagli del database) all'utente. In un contesto di sviluppo locale, questo è conveniente per il debugging. In produzione, è un'information disclosure che un attaccante usa per pianificare l'attacco.
4. Upload di file senza validazione del tipo MIME (15% degli snippet con upload). L'AI genera endpoint di upload che salvano il file senza verificare che sia realmente un'immagine (o il tipo previsto) - controlla al massimo l'estensione del nome file, che è triviale da falsificare. Un attaccante che carica un file avatar.php.jpg e che il server salva con il nome originale può eseguire codice PHP sul server.
5. Token e credenziali hard-coded (5% degli snippet). L'AI genera codice con chiavi API, token e credenziali hard-coded nel codice sorgente - $stripe = new Stripe('sk_live_...') invece di $stripe = new Stripe(config('stripe.secret')). Se il codice viene committato (cosa che con Copilot autocompletamento avviene facilmente), le credenziali finiscono nel repository Git e sono accessibili a chiunque abbia accesso al repository.
Nel mio profilo professionale trovi il dettaglio dell'esperienza che porto negli audit di sicurezza del codice AI-generated - un'area dove la competenza offensiva (sapere come sfruttare ogni vulnerabilità) è il prerequisito per una review efficace.
Il processo di audit per codice AI-generated: cosa aggiungere alla review
La review del codice AI-generated richiede un supplemento specifico rispetto alla review del codice scritto da umani, perché i pattern di errore dell'AI non corrispondono ai pattern che un reviewer umano è abituato a cercare. Il processo che ho standardizzato per i team che usano Copilot e ChatGPT ha tre livelli:
Il primo livello è la regola organizzativa: tutto il codice AI-generated deve essere committato con un tag identificativo nel messaggio di commit o con un commento nel codice che indica che è stato generato da AI. Questo non è burocrazia - è un'informazione che il reviewer usa per calibrare la sua attenzione. Quando il reviewer vede il tag AI, sa che deve controllare con più attenzione i pattern specifici dell'AI (concatenazione di stringhe, assenza di validazione, die() nelle catch, credenziali hard-coded).
Il secondo livello è l'analisi statica mirata: PHPStan al livello massimo cattura molti degli errori di tipo e di accesso che l'AI introduce. Ma per le vulnerabilità di sicurezza - SQL injection, XSS, path traversal - servono strumenti specifici come Psalm con il plugin psalm-plugin-symfony o vimeo/psalm con le taint analysis abilitate, che tracciano il flusso dei dati dall'input utente all'output e segnalano quando un dato non sanitizzato raggiunge un punto sensibile (query SQL, output HTML, comando shell).
Il terzo livello è la checklist manuale che aggiungo alla review. Per ogni snippet identificato come AI-generated, il reviewer deve verificare: tutte le query usano prepared statement o Eloquent (nessuna concatenazione di stringhe), tutti i parametri utente sono validati con FormRequest o con validate(), la gestione degli errori usa exception handler (nessun die(), nessun echo dell'eccezione), gli upload verificano il MIME type reale del file (non solo l'estensione), e nessuna credenziale è hard-coded nel codice.
Ho integrato questa checklist nel bot di code review automatizzato con Claude API che installo nelle pipeline GitHub dei clienti - l'ironia di usare un'AI per verificare il codice di un'altra AI non mi sfugge, ma nella pratica funziona: il bot di review è calibrato con un prompt specifico per cercare i pattern di vulnerabilità dell'AI, e cattura il 70% delle vulnerabilità prima della review umana.
Il paradosso della produttività: perché il codice AI è più rischioso proprio perché funziona
Il rischio più sottile del codice AI-generated non è la vulnerabilità in sé - è la falsa confidenza che il codice funzionante produce nello sviluppatore. Quando Copilot genera un endpoint completo che compila, che risponde con i dati corretti, e che supera i test funzionali (che tipicamente testano solo il caso felice), lo sviluppatore lo committa con la sensazione che "funziona, è a posto." Ma "funziona" non significa "sicuro," e i test funzionali non testano i casi di attacco - non inviano input con SQL injection, non caricano file PHP mascherati da immagini, non verificano che i messaggi di errore non espongano informazioni interne.
La regola che insegno ai team è: il codice AI-generated richiede più review, non meno. Il risparmio di tempo nella scrittura (l'AI genera il codice in secondi) deve essere reinvestito nella review (che il developer fa in minuti). Il bilancio netto è comunque positivo - scrivere 30 minuti + review 5 minuti è meno di scrivere 45 minuti senza AI - ma solo se la review viene fatta. Senza review, il codice AI diventa un generatore di debito tecnico e di vulnerabilità a velocità industriale - molto più veloce di quanto un umano potrebbe produrre scrivendo a mano.
La differenza tra i modelli: Claude, GPT-4 e Copilot producono errori diversi
Un'osservazione che ho fatto durante l'analisi dei 200 snippet è che i diversi modelli producono pattern di vulnerabilità diversi - il che suggerisce che il training data e il fine-tuning influenzano il profilo di sicurezza dell'output. Gli snippet generati da GitHub Copilot (basato su Codex/GPT) avevano il tasso più alto di concatenazione SQL (15% vs 8% per ChatGPT-4) - probabilmente perché Copilot è addestrato su un corpus di codice GitHub che include enormi quantità di codice PHP vecchio con concatenazione di stringhe. ChatGPT-4 produceva meno SQL injection ma più errori nella gestione degli errori (die() e var_dump) - probabilmente perché il modello è fine-tuned per la conversazione e tende a produrre codice "didattico" con output di debug visibili, adatto a un tutorial ma non a un ambiente di produzione.
Claude, nella mia esperienza con l'API Anthropic, produce il codice PHP più sicuro tra i tre - con un tasso di vulnerabilità del 12% contro il 23% della media. La ragione probabile è che Claude è stato addestrato con un focus maggiore sulla sicurezza e sulla "correttezza" dell'output piuttosto che sulla sola funzionalità. Ma il 12% è comunque un tasso di errore non trascurabile - e conferma la regola: nessun modello produce codice che può essere committato senza review di sicurezza, indipendentemente dalla sua reputazione o dal suo benchmark.
Un pattern che ho notato e che è specifico di Copilot è la contaminazione del contesto: quando lo sviluppatore ha una query con concatenazione di stringhe nel file corrente (magari in un commento o in un blocco di codice disabilitato), Copilot tende a replicare quel pattern nelle suggestion successive - amplificando un errore esistente invece di correggerlo. Questo comportamento rende Copilot particolarmente pericoloso nelle codebase legacy dove il codice circostante è insicuro: l'AI non migliora la qualità del codice, la replica. Il consiglio operativo è: se usi Copilot su una codebase legacy con codice insicuro, disabilita Copilot nelle aree con vulnerabilità note fino a quando non le hai corrette - altrimenti l'AI genererà nuove istanze delle stesse vulnerabilità.
L'OWASP Top 10 2025 ha riconosciuto questo rischio aggiungendo i riferimenti alla sicurezza del codice AI-assisted nella categoria di Insecure Design - confermando che il problema non è marginale ma strutturale. Se il tuo team usa Copilot o ChatGPT per generare codice PHP e non ha un processo di review specifico per l'output AI, contattami per definire insieme il workflow di audit: in una giornata analizziamo i commit AI-generated dell'ultimo trimestre, identifichiamo i pattern di vulnerabilità ricorrenti, e definiamo la checklist e gli strumenti di analisi statica che il team integra nel processo di review dal giorno successivo. Il costo della review è una frazione del costo di un incidente di sicurezza causato da codice AI non verificato - e i numeri che ho presentato in questo articolo dimostrano che il rischio non è teorico: il 23% degli snippet AI-generated contiene vulnerabilità, e il 8% sono vulnerabilità critiche che un attaccante può sfruttare con strumenti standard. La domanda non è se il tuo team debba usare AI per generare codice - è se il tuo team ha un processo per verificare che quel codice sia sicuro prima di metterlo in produzione.