Supply chain security con Composer per Laravel e Symfony: come prevenire typosquatting, dependency confusion e script malevoli
La mattina del 29 marzo 2024, Andres Freund - ingegnere Microsoft che lavorava su un benchmark PostgreSQL - ha notato un ritardo di 500 millisecondi sulle connessioni SSH del suo server di test. Quella curiosità apparentemente banale ha portato alla scoperta della backdoor in xz-utils (CVE-2024-3094), una delle compromissioni più sofisticate della storia del software open source: un maintainer che per due anni aveva costruito fiducia nella comunità prima di inserire codice malevolo nel processo di build della libreria di compressione usata da quasi tutti i sistemi Linux. Se non fosse stato per quei 500 millisecondi di ritardo e la curiosità di un singolo ingegnere, la backdoor sarebbe finita nelle distribuzioni stable di Debian, Ubuntu, Fedora e RHEL, dando all'attaccante accesso SSH root a milioni di server nel mondo.
Quando la notizia è uscita, ho fatto l'unica cosa sensata: ho auditato le dipendenze Composer di tutti i 12 clienti PHP che gestivo in quel momento. Il risultato è stato preoccupante. Tre clienti avevano pacchetti con nomi sospettamente simili a librerie note (potenziale typosquatting), nessuno aveva la policy allow-plugins configurata in composer.json (qualsiasi pacchetto poteva eseguire script arbitrari durante l'installazione), zero clienti avevano una SBOM (Software Bill of Materials), e solo due avevano composer audit nella CI pipeline. La supply chain PHP era completamente non governata. In questo articolo ti racconto le contromisure che ho implementato e che oggi applico come standard su ogni progetto Laravel e Symfony.
Stai cercando un Consulente Informatico esperto per mettere in sicurezza la supply chain del tuo progetto PHP? Nel mio profilo professionale trovi l'esperienza concreta su DevSecOps, audit dipendenze e compliance NIS2. Contattami per una consulenza diretta.
Cosa sono il typosquatting e la dependency confusion e perché minacciano i progetti PHP?
Il typosquatting è la pubblicazione di pacchetti malevoli con nomi quasi identici a librerie legittime: laravel/flamework invece di laravel/framework, guzzlhttp/guzzle invece di guzzlehttp/guzzle. Basta un typo nella riga composer require - o un'allucinazione di un LLM che suggerisce un nome di pacchetto inesistente - e il codice malevolo finisce nel tuo vendor/. Un ricercatore ha dimostrato nel 2016 che pacchetti con nomi sbagliati venivano installati su 17.000 macchine in pochi giorni - e nel 2025 il problema è ancora attuale, con malware trovato ripetutamente nel repository Packagist.
La dependency confusion è più sottile: un attaccante pubblica su Packagist un pacchetto con lo stesso nome di un pacchetto privato interno della tua azienda, ma con un numero di versione più alto. Se la configurazione Composer non specifica esplicitamente che quel pacchetto deve venire dal repository privato, Composer può risolvere la dipendenza dal repository pubblico - installando il pacchetto dell'attaccante. Packagist mitiga parzialmente questo rischio con il vendor prefix reservation, ma la protezione non è completa se non configuri correttamente i repository in composer.json.
Gli script malevoli sono il terzo vettore: Composer può eseguire script PHP arbitrari durante install, update e create-project attraverso le direttive scripts nel composer.json del pacchetto. Un pacchetto che sembra innocuo può avere un post-install-cmd che scarica ed esegue un payload - e se non hai la policy allow-plugins configurata, Composer non ti chiede nemmeno conferma.
La prima contromisura: allow-plugins e blocco degli script
Da Composer 2.2, la direttiva allow-plugins in composer.json controlla quali pacchetti possono registrare plugin Composer (e quindi eseguire codice durante l'installazione). Se non è configurata, Composer chiede conferma interattivamente - ma in CI non c'è nessuno a rispondere, e molte pipeline passano --no-interaction che accetta tutto silenziosamente.
{
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true,
"phpstan/extension-installer": true
},
"sort-packages": true
}
}Ogni pacchetto non elencato viene bloccato. Se un nuovo pacchetto tenta di registrare un plugin, Composer fallisce con un errore esplicito anziché eseguire codice silenziosamente. Questo è il singolo cambiamento con il rapporto effort/impatto più alto nella supply chain Composer.
In CI, l'installazione deve avvenire con --no-scripts per impedire l'esecuzione di qualsiasi script durante il build:
# CI: installazione sicura
composer install --no-dev --prefer-dist --no-interaction --no-scripts
# Gli script necessari (es. artisan optimize) vengono eseguiti come step separato e tracciato
php artisan optimizeLa seconda contromisura: audit automatico in CI con gate bloccante
composer audit - disponibile da Composer 2.4 - controlla le dipendenze installate contro il database PHP Security Advisories e restituisce un exit code diverso da zero se ci sono CVE note. Nella CI deve essere un gate bloccante: se ci sono CVE critiche o alte, il deploy non parte.
# GitHub Actions: gate supply chain
- name: Composer audit
run: |
OUTPUT=$(composer audit --no-dev --format=json 2>/dev/null)
CRIT=$(echo "$OUTPUT" | jq '[.advisories[] | select(.cve_severity=="critical" or .cve_severity=="high")] | length')
if [ "$CRIT" -gt 0 ]; then
echo "::error::$CRIT CVE critiche/alte trovate nelle dipendenze"
echo "$OUTPUT" | jq '.advisories[]'
exit 1
fiLa chiave è il filtraggio per severità: blocco su critical e high, segnalo (senza bloccare) su medium e low. Bloccare su tutte le severità rende la CI inutilizzabile perché ci sono quasi sempre advisory low che non rappresentano un rischio reale nel contesto dell'applicazione. Ho descritto l'intero workflow CI/CD con security gate nell'articolo sulla checklist di hardening NIS2-ready per Laravel e Symfony.
La terza contromisura: SBOM per tracciabilità e compliance
Una SBOM (Software Bill of Materials) è l'inventario completo di tutte le dipendenze - dirette e transitive - del tuo progetto, con versioni, licenze e hash. La NIS2 richiede la gestione della supply chain, e una SBOM è lo strumento standard per dimostrare cosa c'è dentro il tuo software.
Lo strumento che uso per PHP è CycloneDX Composer Plugin:
# Generare SBOM in formato CycloneDX JSON
composer require --dev cyclonedx/cyclonedx-php-composer
vendor/bin/cyclonedx-php-composer make-sbom --output-format json --output-file sbom.jsonLa SBOM generata va archiviata come artefatto della CI accanto al build - quando una nuova CVE viene scoperta, puoi cercare nel tuo archivio SBOM se quella dipendenza è presente in qualsiasi versione deployata, senza dover fare git checkout di ogni branch.
Proxy Packagist privato: il livello enterprise
Per PMI che sviluppano internamente pacchetti PHP privati - utility condivise tra progetti, SDK interni, adapter per sistemi legacy - un proxy Packagist privato è la contromisura più completa contro la dependency confusion. Il proxy funge da intermediario tra i tuoi progetti e il repository pubblico: tutti i composer install passano dal proxy, che mantiene una cache locale dei pacchetti approvati e rifiuta pacchetti non in whitelist.
Le opzioni sono Private Packagist (servizio gestito, da 49$/mese) e Satis (self-hosted, gratuito ma richiede manutenzione). Per la maggior parte delle PMI, Private Packagist è la scelta migliore perché elimina il costo di gestione. La configurazione in composer.json:
{
"repositories": [
{
"type": "composer",
"url": "https://repo.packagist.com/acme-srl/",
"only": ["acme-srl/*"]
},
{
"type": "composer",
"url": "https://repo.packagist.org",
"canonical": true
}
]
}La keyword only e canonical sono fondamentali: only limita il repository privato ai soli pacchetti con vendor prefix acme-srl/, e canonical su Packagist.org indica che per tutti gli altri pacchetti Packagist è l'unica fonte. Questo impedisce che un pacchetto pubblico con lo stesso nome di un pacchetto privato venga installato dal repository sbagliato.
Cosa ho trovato durante l'audit post-xz-utils
Dei 12 clienti auditati, i tre casi di nomi sospetti erano: un pacchetto monolog/monolog-bridge che non esiste su Packagist (il pacchetto legittimo è symfony/monolog-bridge), un guzzle/guzzle (il pacchetto legittimo è guzzlehttp/guzzle), e un laravel/helpers installato da un repository GitHub diretto anziché da Packagist. Nessuno dei tre era effettivamente malevolo - erano errori di digitazione del sviluppatore precedente che per coincidenza avevano trovato pacchetti omonimi su Packagist - ma il fatto che fossero passati inosservati per mesi dimostrava l'assenza totale di governance.
L'audit ha prodotto per ogni cliente un report con: inventario completo delle dipendenze (dirette e transitive), verifica del vendor prefix di ogni pacchetto, controllo delle CVE note, verifica della configurazione allow-plugins, e una SBOM iniziale. Quel report - generato in meno di due ore per cliente - è diventato la baseline per il monitoring continuo che oggi gira in CI su ogni push.
La quarta contromisura: governance degli aggiornamenti
L'errore che ha causato l'incidente dell'articolo 219 - una dipendenza Composer aggiornata automaticamente dal CI con una versione contenente backdoor - nasceva dall'assenza di governance sugli aggiornamenti. composer update in CI senza review umana è equivalente a dare a qualsiasi maintainer upstream il diritto di modificare il tuo codice di produzione.
La mia regola è: composer install (da lockfile) in CI e in produzione. Mai composer update. Gli aggiornamenti avvengono in un branch dedicato, con diff del composer.lock visibile nella PR, review umana del changelog delle dipendenze aggiornate, e merge solo dopo che i test passano. Per dipendenze critiche (framework, ORM, driver database), leggo il changelog e il diff del codice sorgente prima di approvare l'aggiornamento.
Dependabot e Renovate possono automatizzare la creazione delle PR di aggiornamento, ma la merge deve restare manuale e con review. L'automazione crea la PR, l'umano la valida - mai il contrario.
Ho descritto gli attacchi alla supply chain software in un contesto più ampio - inclusa la compromissione di tj-actions/changed-files del marzo 2025 e le lezioni post-xz-utils - nell'articolo sugli attacchi alla supply chain e la protezione per PMI.
Se il tuo progetto Laravel o Symfony non ha allow-plugins configurato, non ha composer audit in CI, e aggiorna le dipendenze senza review - la tua supply chain è un vettore di attacco aperto. Le contromisure che ho descritto richiedono meno di un giorno di lavoro e trasformano una superficie di attacco non governata in un processo controllato e auditabile. La supply chain non è un problema che risolvi una volta: è un processo continuo. Le dipendenze si aggiornano, nuove CVE vengono scoperte, nuovi pacchetti vengono aggiunti. La differenza tra un progetto vulnerabile e uno resiliente non è l'assenza di rischio - è la capacità di rilevare e reagire rapidamente quando il rischio si materializza. allow-plugins, composer audit in CI, SBOM e governance degli aggiornamenti sono i quattro pilastri che trasformano una supply chain cieca in una controllata. Per la NIS2, questa tracciabilità non è un bonus - è un requisito: l'articolo 21 richiede esplicitamente la gestione della sicurezza della supply chain, e una SBOM con audit automatizzato è l'evidenza più forte che puoi produrre.
Contattami se vuoi un audit della supply chain del tuo progetto PHP - il primo passo è sempre un inventario di ciò che hai installato e una baseline da cui partire per il monitoring continuo.