Sicurezza PHP
Sicurezza PHP: prevenire le vulnerabilità PHP nel codice, nelle dipendenze Composer e nella build
Proteggere PHP significa lavorare su tre livelli. Nel codice che scrive: validi l'input, usi query parametrizzate e l'escaping del framework, e non deserializzi mai dati non attendibili. Nei pacchetti Composer da cui dipende: esegua composer audit, faccia commit di composer.lock e blocchi le versioni insicure. Nella CI che li installa: registri ciò che è stato davvero installato e limiti dove la build può connettersi. La maggior parte delle guide alla sicurezza PHP si ferma al primo livello. Questa li copre tutti e tre.
Aggiornato il
- Il suo codice Injection, XSS, deserializzazione, gestione dei file, sessioni Impostazioni predefinite del framework, code review, PHPStan, Psalm, Semgrep
- Le sue dipendenze Composer Vulnerabilità note, pacchetti abbandonati e malevoli, deriva del lockfile, codice eseguito in fase di installazione composer audit, policy di Composer, Dependabot o Renovate
- La sua build Cosa la CI ha davvero installato ed eseguito, da dove l'ha scaricato, cosa poteva raggiungere Action fissate, token con privilegi minimi, egress policy, SBOM in fase di build
In questa pagina: PHP2Shell, le vulnerabilità PHP che contano ancora, configurazione e versioni supportate, analisi statica, sicurezza delle dipendenze Composer, attacchi alla supply chain di PHP, la build CI, SBOM e strumenti per livello.
PHP2Shell: la vulnerabilità PHP da correggere subito
Il 21 ottobre 2026 il progetto PHP ha rilasciato PHP 8.5.12, 8.4.27, 8.3.36 e 8.2.35 per correggere due vulnerabilità critiche nello stesso PHP che possono consentire l'esecuzione di codice da remoto senza autenticazione. Entrambe hanno un punteggio di 9,2 in CVSS v4.0 e insieme sono note come PHP2Shell:
- CVE-2026-109618: la gestione dell'esaurimento della memoria può interrompere PHP mentre i dati della richiesta sono inizializzati solo in parte, e la pulizia della richiesta può poi trattare quei dati non validi come validi.
- CVE-2026-109619: lo stato appartenente a una richiesta può sopravvivere dopo che la sua memoria è stata liberata, e la richiesta successiva gestita dallo stesso worker può usarlo.
L'advisory di PHP afferma che ogni installazione che serve richieste web è a rischio, inclusi PHP-FPM, Apache con mod_php, php-cgi, FrankenPHP e LiteSpeed, e considera mod_php il caso a rischio più alto. PHP 8.1 e versioni precedenti non ricevono nuove release dal progetto PHP. Data la gravità, però, il team di sicurezza di PHP ha pubblicato backport minimi del codice sorgente per PHP 7.4, 8.0 e 8.1, per chi compila PHP dai sorgenti o non ha un fornitore LTS, e alcune distribuzioni e fornitori LTS distribuiscono correzioni proprie. Cosa fare:
- Aggiorni a una release corretta. Su 8.1 o precedenti, passi a un ramo supportato, installi il pacchetto con il backport del suo fornitore o applichi il backport ufficiale dei sorgenti se compila PHP da sé.
-
Ricostruisca ogni immagine container che include PHP e la ridistribuisca. Un vecchio tag di immagine continua a
eseguire il vecchio PHP finché non viene sostituito, quindi controlli ciò che è in esercizio, non solo ciò che c'è
nel repository:
docker run --rm IMAGE php -v. -
Se oggi non può aggiornare, applichi le mitigazioni temporanee dell'advisory di sicurezza di PHP, come
session.upload_progress.enabled=0. Lo imposti inphp.ini, nel pool FPM o in una configurazione del server applicata prima del parsing della richiesta, e riavvii ogni worker. Non si affidi aini_set()o al.user.inidi FPM: l'advisory sconsiglia entrambi. PHP è chiaro: queste mitigazioni riducono l'esposizione ma non rendono sicura un'installazione.
Noti che cosa composer audit non può dirle in questo caso: PHP2Shell è nel runtime, non in un pacchetto
Composer, quindi non compare mai in composer.lock. Il PHP che esegue proviene dall'immagine base o dai
pacchetti del sistema operativo, cioè dal terzo livello di questa guida. Per la tabella delle versioni, la cronologia e
uno script che controlla ogni tag di immagine, veda
PHP2Shell: cosa correggere e come trovare ogni PHP vulnerabile.
Le vulnerabilità PHP che contano ancora
Le classi di vulnerabilità delle applicazioni PHP sono cambiate poco in dieci anni. Quello che è cambiato è che Laravel, Symfony e il PHP moderno ne chiudono la maggior parte per impostazione predefinita, quindi i bug reali oggi si concentrano dove il codice aggira quelle impostazioni. L'injection è A05 nell'OWASP Top 10:2025, e Software Supply Chain Failures entra come novità in A03, il tema della seconda metà di questa guida.
| Vulnerabilità | Come si presenta in PHP | La correzione | Laravel e Symfony |
|---|---|---|---|
| SQL injection | Dati della richiesta concatenati nell'SQL, comprese le scorciatoie verso query raw | Prepared statement PDO con parametri associati | Eloquent e Doctrine associano i parametri; whereRaw(), DB::raw() e il DQL concatenato no |
| Cross-site scripting (XSS) | Input dell'utente stampato nell'HTML senza escaping | Escaping in output con htmlspecialchars() o con il motore di template |
Blade {{ }} e Twig fanno escaping per impostazione predefinita; {!! !!} e |raw no |
| Cross-site request forgery (CSRF) | Un form o un endpoint AJAX che modifica lo stato accetta qualsiasi richiesta con il cookie di sessione | Un token per sessione verificato a ogni POST, PUT, PATCH e DELETE, più cookie SameSite |
Laravel verifica il token sulle route web (@csrf nei form); i Symfony Forms ne includono e verificano uno per impostazione predefinita; le route escluse dalla verifica restano senza protezione |
Code injection tramite eval() |
Dati della richiesta, o un template o una formula modificabili dall'utente, che arrivano a eval() |
Non usare eval(); mappare l'input su un insieme fisso di operazioni, o usare un parser per le espressioni necessarie |
Nessun framework protegge le sue chiamate a eval() |
| Object injection | unserialize() su cookie, dati della richiesta o voci di cache che un attaccante può scrivere |
Usi JSON per i dati esterni; se deve deserializzare, rifiuti gli oggetti | Nessun framework protegge le sue chiamate a unserialize() |
| Inclusione di file e upload | Un percorso costruito dall'input e passato a include; upload salvati sotto la web root con il nome di file del client |
Allow-list dei file inclusi; salvi gli upload fuori dalla web root con nomi generati | store() di Laravel genera i nomi dei file; validi tipo e dimensione sul server |
| Type juggling | == per confrontare token o hash, dove "0e123" == "0e456" è vero |
===, hash_equals() per i segreti, password_verify() per le password |
Gli helper del framework confrontano in modo sicuro; i suoi confronti dipendono da lei |
| Sessioni e cookie deboli | ID di sessione leggibili da JavaScript, inviati su HTTP o mantenuti dopo il login | Cookie Secure, HttpOnly e SameSite, strict mode, un nuovo ID di sessione dopo il login | Entrambi rigenerano l'ID di sessione al login |
| Server-side request forgery (SSRF) | file_get_contents() o cURL su un URL fornito dall'utente, inclusi wrapper come file:// e phar:// |
Allow-list di schemi e host, blocco degli indirizzi interni, allow_url_fopen disattivato dove non serve |
Nessuna protezione predefinita |
| Command injection | Input passato a exec(), shell_exec(), system() o proc_open() |
Eviti la shell; passi gli argomenti come array o faccia l'escape di ciascuno con escapeshellarg() |
Symfony Process e la facade Process di Laravel accettano un array di argomenti |
| Mass assignment | User::create($request->all()) consente a una richiesta di impostare is_admin |
Elenchi i campi compilabili o passi solo l'input validato | I modelli Eloquent sono protetti per impostazione predefinita; $guarded = [] disattiva la protezione |
SQL injection: vulnerabile e corretto
// Vulnerable: the id goes straight into the SQL string
$rows = $pdo->query("SELECT * FROM orders WHERE id = " . $_GET['id']);
// Fixed: a prepared statement with a bound parameter
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([$_GET['id']]);
$rows = $stmt->fetchAll();
// Laravel: whereRaw() takes SQL as written, where() binds the value
Order::whereRaw("status = '" . $request->input('status') . "'")->get(); // vulnerable
Order::where('status', $request->input('status'))->get(); // fixed
XSS: vulnerabile e corretto
<?php // Plain PHP: escape on output
echo '<p>' . $comment . '</p>'; // vulnerable
echo '<p>' . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8') . '</p>'; // fixed ?>
{{-- Blade: {{ }} escapes, {!! !!} prints raw HTML --}}
<p>{!! $comment !!}</p> {{-- vulnerable --}}
<p>{{ $comment }}</p> {{-- fixed --}}
{# Twig: autoescape is on, |raw turns it off #}
<p>{{ comment|raw }}</p> {# vulnerable #}
<p>{{ comment }}</p> {# fixed #}
Object injection: vulnerabile e corretto
// Vulnerable: an attacker-controlled cookie becomes PHP objects, and any
// class with a __wakeup() or __destruct() method in vendor/ is a gadget
$prefs = unserialize($_COOKIE['prefs']);
// Fixed: JSON carries data, not objects
$prefs = json_decode($_COOKIE['prefs'], true, 512, JSON_THROW_ON_ERROR);
// If you must read serialized data, refuse objects
$prefs = unserialize($data, ['allowed_classes' => false]);
La deserializzazione phar, la variante in cui un'operazione su file con un percorso phar://
deserializzava i metadati dell'archivio, è stata ristretta in PHP 8.0: i metadati phar
non vengono più deserializzati
all'apertura di un phar, ma solo quando il codice chiama getMetadata(). Un motivo in più per lasciarsi PHP
7 alle spalle.
Inclusione di file: vulnerabile e corretto
// Vulnerable: ?page=../admin/reset reaches any .php file on the disk
include __DIR__ . '/pages/' . $_GET['page'] . '.php';
// Fixed: only names on an allow-list can be included
$pages = ['home' => 'home.php', 'pricing' => 'pricing.php'];
$file = $pages[$_GET['page'] ?? 'home'] ?? 'home.php';
include __DIR__ . '/pages/' . $file;
Configurazione e hardening del runtime
Un'applicazione sicura può comunque esporre informazioni attraverso il suo runtime. Queste impostazioni di
php.ini seguono l'OWASP
PHP Configuration Cheat Sheet:
| Impostazione | Valore in produzione | Perché |
|---|---|---|
display_errors |
Off (con log_errors = On) |
Gli stack trace mostrano percorsi, query e a volte segreti |
expose_php |
Off |
Smette di dichiarare la versione esatta di PHP negli header di risposta |
allow_url_include |
Off |
Impedisce l'include di URL remoti; deprecato da PHP 7.4 |
allow_url_fopen |
Off se l'applicazione non scarica URL con le funzioni sui file |
Riduce l'SSRF tramite file_get_contents() |
disable_functions |
Per esempio exec, shell_exec, system, passthru, proc_open, popen |
Toglie la shell a un attaccante che riesce a eseguire codice; elenchi solo ciò che la sua applicazione non chiama mai |
open_basedir |
La directory dell'applicazione | Limita i percorsi che le funzioni sui file di PHP possono aprire; un parapetto, non un confine di isolamento |
| Cookie di sessione | session.cookie_secure = 1, session.cookie_httponly = 1, session.cookie_samesite = Lax o Strict, session.use_strict_mode = 1 |
Tiene gli ID di sessione lontani da HTTP in chiaro e da JavaScript, e rifiuta gli ID che il server non ha emesso |
file_uploads |
Off se l'applicazione non accetta upload |
Elimina un intero canale di input |
Resti su una versione di PHP supportata
Secondo php.net, PHP offre a ogni ramo due anni di supporto attivo e poi due anni di sole correzioni di sicurezza. PHP 8.1 ha raggiunto la fine del ciclo di vita il 31 dicembre 2025 e PHP 8.2 la raggiungerà il 31 dicembre 2026. Un ramo a fine vita non riceve nuove release dal progetto PHP, ed è la situazione in cui PHP2Shell ha messo gli utenti della 8.1: l'unica correzione ufficiale per loro è stata un backport dei sorgenti.
- PHP 8.1 Fine del ciclo di vita, terminato il 31 dic 2025
- PHP 8.2 Solo correzioni di sicurezza, fino al 31 dic 2026
- PHP 8.3 Solo correzioni di sicurezza, fino al 31 dic 2027
- PHP 8.4 Supporto attivo, fino al 31 dic 2026
- PHP 8.5 Supporto attivo, fino al 31 dic 2027
Impostazioni di produzione per Laravel e Symfony
-
Debug disattivato.
APP_DEBUG=falsein Laravel,APP_ENV=prodeAPP_DEBUG=0in Symfony. Una pagina di debug in produzione mostra la configurazione e i valori dell'ambiente. -
Chiavi e segreti. Tenga
APP_KEY(Laravel) eAPP_SECRET(Symfony) fuori dal repository e li ruoti se trapelano.APP_PREVIOUS_KEYSdi Laravel le consente di ruotare la chiave senza disconnettere tutti gli utenti. -
Proxy attendibili. Dietro un load balancer, elenchi i proxy di cui si fida (il middleware dei
trusted proxies di Laravel,
framework.trusted_proxiesin Symfony), così un client non può falsificare il proprio indirizzo IP o HTTPS con gli headerX-Forwarded-*.
Analisi statica per PHP (SAST)
L'analisi statica legge il codice senza eseguirlo. Tre strumenti open source coprono la maggior parte dei team PHP:
-
PHPStan trova errori di tipo e bug con livelli di rigore crescenti. Non è uno scanner di sicurezza,
ma molti bug di sicurezza nascono da una confusione di tipi o da un
nullnon controllato. -
Psalm aggiunge la taint
analysis: segue i dati da sorgenti come
$_GETe$_COOKIEfino a sink comeecho,includee le query SQL, e segnala problemi comeTaintedSqleTaintedHtml. Il Psalm attuale la esegue insieme al controllo dei tipi;--taint-analysisla attiva per una singola esecuzione, e--report=results.sarifproduce SARIF per GitHub code scanning. - Semgrep supporta PHP come linguaggio in disponibilità generale, con regole della community per gli errori PHP più comuni e analisi del flusso di dati tra funzioni nella versione commerciale.
La SAST è rumorosa sul PHP legacy per ragioni strutturali: include dinamici, variabili variabili, metodi magici come
__get() e __call(), e convenzioni dei framework che fanno passare i dati attraverso container
e facade che l'analizzatore non riesce a seguire. Lo strumento o tira a indovinare, e segnala falsi positivi, o si
arrende, e manca flussi reali.
Su una codebase datata funziona meglio come un cricchetto. Registri i risultati di oggi come baseline
(vendor/bin/phpstan analyse --generate-baseline scrive phpstan-baseline.neon; Psalm ha
--set-baseline), faccia fallire la build solo sui nuovi risultati e alzi il livello man mano che riduce la
baseline. I team con codebase PHP di lunga data ci raccontano che l'autofix basato su IA funziona bene per gli
aggiornamenti delle dipendenze ed è discontinuo sui risultati relativi al codice, quindi riveda le correzioni generate
per i risultati SAST con la stessa cura di qualsiasi altra modifica. SAST e SCA rispondono a domande diverse; veda
SAST vs SCA.
Sicurezza delle dipendenze Composer
Un'applicazione Laravel o Symfony installa decine di pacchetti Composer, la maggior parte transitivi. Composer ha acquisito vere funzionalità di sicurezza nelle ultime release; questa sezione si riferisce a Composer 2.11, la release attuale.
composer audit
composer audit confronta i pacchetti installati con gli advisory di sicurezza
dell'API di Packagist.org, che
raccoglie advisory da fonti come la GitHub Advisory Database e FriendsOfPHP/security-advisories. Segnala anche i
pacchetti abbandonati e, da Composer 2.10, le versioni di pacchetti contrassegnate come malware. Veda la
documentazione di Composer per tutte le
opzioni.
composer audit # audit what is installed in vendor/
composer audit --locked # audit composer.lock, whatever is in vendor/
composer audit --no-dev # skip require-dev packages
composer audit --direct # direct dependencies only (Composer 2.11)
composer audit --format=json # table, plain, json or summary
composer audit --abandoned=report # ignore, report or fail on abandoned packages
composer audit --ignore-severity=low
Da Composer 2.10 il codice di uscita è 0 quando non ci sono problemi e 1 quando qualcosa non
supera l'audit, quindi funziona come gate di CI senza dover analizzare l'output. I pacchetti abbandonati fanno fallire
l'audit per impostazione predefinita.
I risultati accettati vanno in composer.json, con una motivazione, e non in un flag di CI che nessuno
ricorda. Composer 2.10 ha sostituito la maggior parte della vecchia configurazione audit con un blocco
policy:
{
"config": {
"policy": {
"advisories": {
"ignore-id": {
"CVE-1234": "Not affected: we never call the vulnerable parser."
}
},
"abandoned": {
"audit": "report"
}
}
}
}
Le chiavi legacy (audit.ignore, audit.abandoned, audit.block-insecure)
funzionano ancora come fallback finché manca la sezione policy corrispondente, ma sono deprecate. Migri
tutte le impostazioni sugli advisory insieme: appena esiste policy.advisories, ogni chiave
audit.* relativa agli advisory viene ignorata.
Bloccare le versioni insicure
Da Composer 2.9 (novembre 2025), composer update e composer require rifiutano le versioni di
pacchetti che hanno un advisory di sicurezza. Era l'impostazione audit.block-insecure e ora è
policy.advisories.block, attiva per impostazione predefinita. Composer 2.10 ha aggiunto il blocco del
malware basato sul feed malware di Packagist, che si applica anche a composer install. Composer 2.11 ha
aggiunto una policy cooldown opzionale che trattiene le versioni appena rilasciate per un periodo a sua
scelta, come "7 days", perché molte versioni malevole vengono individuate e rimosse nel giro di ore o
giorni.
In CI contano due dettagli. Primo, il blocco basato sugli advisory si applica quando le versioni vengono risolte: una
versione già presente in composer.lock viene comunque installata da composer install dopo la
pubblicazione di un advisory, quindi serve ancora composer audit come gate. Secondo,
--no-blocking disattiva tutte le policy per un comando: vale la pena cercarlo nelle sue pipeline.
Prima di Composer 2.9, il modo abituale per bloccare le versioni insicure era
roave/security-advisories, un pacchetto che
dichiara un conflict con ogni versione vulnerabile nota, a partire dalla GitHub Advisory Database e da
FriendsOfPHP. Funziona ancora ed è ancora utile con versioni di Composer più vecchie; con Composer 2.9 o successivo, il
blocco integrato copre gli stessi advisory senza il pacchetto aggiuntivo.
Disciplina del lockfile
- Faccia commit di
composer.lockper le applicazioni, così ogni ambiente installa le stesse versioni. -
Usi
composer installin CI e nei deploy, maicomposer update, che risolve nuove versioni al volo. - Installi le build di produzione con
--no-dev, così gli strumenti di test non vengono mai rilasciati. -
Esegua
composer validate --strict: fallisce quandocomposer.locknon è allineato concomposer.json.
Un lockfile registra ciò che Composer ha risolto. Veda cosa un lockfile non registra.
Codice in fase di installazione: plugin, script e file caricati in autoload
Composer esegue codice delle sue dipendenze in più punti di quanto la maggior parte dei team si aspetti, il che lo rende l'equivalente PHP degli script di installazione di npm:
-
I plugin girano dentro Composer stesso. Da Composer 2.2, l'impostazione
allow-pluginsdeve elencare ogni plugin autorizzato a girare, e per impostazione predefinita non ne autorizza nessuno. Tratti ogni nuova voce lì come una nuova dipendenza con accesso alla shell. -
Gli script vengono eseguiti solo dal
composer.jsonprincipale, non dalle dipendenze. Ma spesso avviano la sua applicazione. -
I file in autoload vengono eseguiti ogni volta che si carica l'autoloader. In un'applicazione
Laravel standard,
composer installtermina conphp artisan package:discover, che carica l'autoloader, quindi ifilesin autoload di una dipendenza vengono eseguiti nel suo job di CI durante l'installazione.
Se un job non ha bisogno di plugin o script, --no-plugins --no-scripts impedisce che il codice delle
dipendenze giri durante l'installazione; esegua poi esplicitamente i passaggi che le servono.
Sorgenti dei repository e dependency confusion
Composer può installare da Packagist.org, da repository Composer privati come Private Packagist, da repository VCS
(URL Git) e da percorsi locali. Mescolare sorgenti private e pubbliche è il terreno della
dependency confusion: se il nome del suo pacchetto privato esiste anche su
Packagist.org con una versione più alta, quale riceve? In Composer 2
ogni repository è
canonico per impostazione predefinita: quando Composer trova un pacchetto in un repository, non lo cerca più in
basso nella lista, e Packagist.org è sempre l'ultimo. Impostare "canonical": false su un repository
elimina questa protezione, quindi lo faccia solo intenzionalmente. I repository VCS scaricano ciò che l'host Git
serve per un tag, quindi li fissi per commit nel lockfile e ne riveda le modifiche.
Pacchetti abbandonati e non mantenuti
I parchi applicativi PHP legacy si portano dietro pacchetti i cui maintainer sono passati ad altro. Packagist segna un
pacchetto come abbandonato quando il maintainer lo dichiara, spesso indicando un sostituto, e
composer audit fallisce su di esso per impostazione predefinita. Molti pacchetti non mantenuti però non
vengono mai abbandonati formalmente. Controlli la data dell'ultima release di tutto ciò che gestisce input,
autenticazione o crittografia, e pianifichi le sostituzioni prima che sia una vulnerabilità a imporle.
Aggiornare in sicurezza quando i test sono pochi
Il caso più difficile è una grande applicazione legacy con una dipendenza vulnerabile e pochi test. Ciò che funziona:
-
Aggiorni il pacchetto che porta dentro quello vulnerabile. La maggior parte dei risultati è
transitiva; esegua
composer why vendor/packageper vedere la catena e aggiorni prima il pacchetto a monte entro il suo vincolo. -
Aggiorni una cosa alla volta.
composer update vendor/package --with-dependenciesmantiene la modifica abbastanza piccola da rivederla nel diff dicomposer.lock. - Testi prima i percorsi critici. Login, checkout, i principali endpoint API: una manciata di controlli end-to-end intercetta la maggior parte dei problemi da aggiornamento.
- Aggiunga test di caratterizzazione attorno a ciò che l'aggiornamento tocca. Registri cosa fa il codice oggi, così si accorge quando l'aggiornamento lo cambia.
- Distribuisca per fasi e osservi i tassi di errore prima di raggiungere tutti i clienti.
Cosa non vede composer audit
Il lockfile è un piano, non un registro della build. composer audit legge vendor/ o
composer.lock, quindi non vede:
- PHP stesso e le sue estensioni, che provengono dall'immagine base o dai pacchetti del sistema operativo (PHP2Shell ne è un esempio).
- Estensioni PECL e librerie di sistema installate nell'immagine Docker.
- Strumenti che la CI scarica con
curl, action che esegue e binari scaricati da un plugin o da uno script. - Codice malevolo che non ha ancora un advisory o un contrassegno di malware.
Gli stessi limiti valgono per npm audit e pip-audit nei rispettivi ecosistemi.
Attacchi alla supply chain di PHP
Quattro incidenti mostrano dove colpiscono davvero gli attacchi alla supply chain contro PHP, e cosa avrebbe intercettato ciascuno.
Marzo 2021: commit malevoli in php-src
Il 28 marzo 2021 sono stati inviati due commit malevoli al repository php-src a nome di due sviluppatori del core. Il team PHP ha annunciato che avrebbe smesso di gestire il proprio server Git e reso canonici i repository su GitHub. Il suo aggiornamento ha stabilito che i commit erano stati inviati via HTTPS con autenticazione tramite password, forse con credenziali provenienti da un database utenti trapelato, e non attraverso un server compromesso. Cosa lo avrebbe intercettato: nulla nella sua pipeline. Il bersaglio era il linguaggio stesso, a ricordare che anche il runtime PHP è una dipendenza.
Aprile 2021: command injection in Composer (CVE-2021-29472)
Un difetto nel modo in cui Composer gestiva gli URL dei repository Mercurial consentiva la command injection. Packagist ha riferito che Packagist.org e Private Packagist erano in teoria esposti tramite URL di pacchetto costruiti ad arte, che non riteneva fosse stato sfruttato, e l'ha corretto in Composer 2.0.13 e 1.10.22. Cosa lo avrebbe intercettato: mantenere aggiornato Composer stesso sulle macchine degli sviluppatori e nelle immagini di CI. Il package manager fa parte della sua supply chain.
Maggio 2023: furto di account su Packagist
Il 1° maggio 2023 un attaccante è entrato in quattro account inattivi di Packagist.org con password trapelate altrove,
che insieme controllavano 14 pacchetti, e ha puntato i pacchetti verso repository forkati. I fork contenevano una
descrizione modificata e nessun codice malevolo.
Packagist li ha
ripristinati la mattina seguente. Cosa lo avrebbe intercettato: un composer.lock nel
repository, perché composer install usa le sorgenti registrate nel lockfile, e la revisione dei diff del
lockfile alla ricerca di URL inattesi, come raccomanda Packagist stesso. Per i maintainer: password uniche e
autenticazione a due fattori.
2026: tag dirottati su laravel-lang e intercom-php
Nel 2026 attaccanti con accessi rubati hanno pubblicato release malevole di pacchetti esistenti. Packagist cita
intercom/intercom-php (30 aprile) e i pacchetti laravel-lang (22 maggio) nel suo
aggiornamento
sulla sicurezza della supply chain. Nel caso laravel-lang, i tag di versione puntavano a commit di un fork
malevolo, e il payload, un file src/helpers.php
caricato
dall'autoloader di Composer, scaricava un credential stealer per chiavi cloud, chiavi SSH e file
.env. Aikido ha contato 233 versioni compromesse. Cosa lo avrebbe intercettato: una
volta contrassegnate, la policy malware di Composer 2.10 blocca quelle versioni anche con
composer install; prima di allora, un periodo di cooldown tiene fuori le versioni appena pubblicate, una
egress policy impedisce al payload di raggiungere il suo server e un registro di ogni build mostra quali build hanno
installato una versione malevola.
Da allora Packagist ha annunciato che rifiuta le versioni stabili riassegnate a un nuovo tag, pubblica un registro di trasparenza delle modifiche di proprietà e dei tag, e contrassegna il malware su Packagist.org da marzo 2026.
Proteggere la build PHP in CI
La build è il punto in cui si incontrano codice, dipendenze e segreti, quindi è lì che un pacchetto malevolo fa più danni. Un workflow GitHub Actions per una build di produzione:
name: build
on: [push, pull_request]
permissions:
contents: read # least privilege for GITHUB_TOKEN
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
with:
php-version: "8.4"
tools: composer:v2
coverage: none
- run: composer validate --strict
- id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ hashFiles('composer.lock') }}
- run: composer install --no-dev --no-interaction --no-progress --optimize-autoloader
- run: composer audit --no-dev
-
Token con privilegi minimi.
permissions: contents: readimpedisce a uno step compromesso di inviare codice o pubblicare release conGITHUB_TOKEN. Veda i permessi del token GitHub. - Action fissate. Uno SHA di commit completo non può essere spostato come un tag; il commento mantiene la versione leggibile per Dependabot o Renovate.
-
Metta in cache i download, non
vendor/. Mettere in cache la cache dei download di Composer fa sì checomposer installgiri sul lockfile in ogni job. Unvendor/ripristinato viene accettato così com'è, e le cache possono essere avvelenate tra workflow; veda la cache di GitHub Actions. - Registri ciò che è stato installato. Il lockfile dice cosa dovrebbe essere installato. Uno SBOM registrato dalla build dice cosa lo è stato davvero, inclusi il runtime PHP, i pacchetti del sistema operativo e i download esterni a Composer. Veda gli SBOM in fase di build.
- Limiti dove la build può connettersi. Una build Composer ha bisogno di Packagist, GitHub e del suo repository privato. Una egress policy che consente solo questi impedisce a un credential stealer di inviare ciò che ha trovato.
Generare uno SBOM per un progetto PHP
L'opzione open source standard è il
plugin CycloneDX per Composer.
Essendo un plugin, richiede una voce in allow-plugins:
composer require --dev cyclonedx/cyclonedx-php-composer
composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json
Legge composer.json insieme ai pacchetti installati o a composer.lock, quindi elenca con
precisione le sue dipendenze Composer e nient'altro. Il runtime PHP, le sue estensioni, i pacchetti apt o apk
dell'immagine base e gli strumenti scaricati dal job non ci sono. Uno SBOM in fase di build registra anche questi,
perché è ricavato da ciò che il job ha scaricato e non dai file di Composer. Per i formati e ciò che uno SBOM deve
contenere, veda cos'è uno SBOM.
Strumenti di sicurezza PHP per livello
| Livello | Strumenti | Cosa coprono | Cosa non vedono |
|---|---|---|---|
| Codice | PHPStan, Psalm, Semgrep | Bug ed errori di tipo, flussi di dati contaminati, pattern insicuri noti | Dipendenze vulnerabili, il runtime, la build |
| Dipendenze | composer audit, policy di Composer, roave/security-advisories, Dependabot o Renovate |
Advisory pubblicati, pacchetti abbandonati e contrassegnati, pull request di aggiornamento | Il suo codice e tutto ciò che è fuori da composer.lock |
| Immagine | Scanner di container | Pacchetti del sistema operativo e del linguaggio in un'immagine costruita | Ciò che la build ha scaricato e non ha conservato, e le immagini che nessuno ha scansionato |
| SBOM | Plugin CycloneDX per Composer | Pacchetti Composer come SBOM CycloneDX | Tutto ciò che è fuori da Composer |
| Build | CRACI | Ciò che ogni job GitHub Actions ha davvero installato, da Composer e da ogni altra sorgente, monitorato nel tempo | Il suo codice PHP (nessuna SAST); sistemi di CI diversi da GitHub Actions |
Di più sulle immagini: scansione dei container. Meno risultati, ma migliori, su tutti i livelli: come ridurre il rumore delle vulnerabilità.
Sicurezza PHP e CRACI
CRACI è un runner GitHub Actions che registra ciò che ogni job installa davvero. Cambi runs-on in
craci e ogni pacchetto Composer scaricato dal job viene registrato con il suo package URL
(pkg:composer/vendor/package), insieme ai pacchetti del package manager del sistema operativo, alle
immagini container, alle sorgenti Git e agli altri download dello stesso job, compresi i pacchetti ripristinati dalle
cache di CI. Ogni componente dello SBOM era nella build.
CRACI confronta questi componenti con le vulnerabilità note e continua a ricontrollare gli SBOM di ciò che ha rilasciato man mano che escono nuovi advisory, così una CVE divulgata l'anno prossimo la porta comunque alle build che la contengono. Le policy gate possono fermare una build in base ai risultati, inclusa una build che contiene una CVE specifica, e la egress policy limita dove il job può connettersi.
CRACI non analizza il suo codice PHP: nessuna SAST, per scelta progettuale. Mantenga PHPStan, Psalm o Semgrep per il livello del codice. Veda ogni pacchetto che la sua build PHP installa.
Quando CRACI non è la risposta
-
Fa il deploy via FTP o su hosting condiviso senza CI. Non c'è una build da registrare. Parta da
composer audite da uncomposer.locknel repository. - Il suo rischio è soprattutto nel suo codice, come in un'applicazione legacy piena di SQL raw. SAST, code review e test vengono prima.
- Esegue le build fuori da GitHub Actions, su GitLab CI o Bitbucket Pipelines. CRACI supporta GitHub Actions oggi; GitLab CI e Jenkins sono nella roadmap.
Sicurezza PHP: domande frequenti
Come si mette in sicurezza il codice PHP?
Lavori su tre livelli. Nel codice, validi l'input, usi prepared statement, si affidi all'escaping dell'output del framework e non deserializzi mai dati non attendibili. Nelle dipendenze, faccia commit di composer.lock, esegua composer audit in CI e mantenga attivo il blocco delle versioni insicure di Composer. Nella build, fissi le action, limiti il token, restringa l'accesso alla rete e registri ciò che il job ha installato.
Quali sono le vulnerabilità PHP più comuni?
SQL injection, cross-site scripting, cross-site request forgery, code injection tramite eval(), object injection tramite unserialize(), inclusione di file e upload insicuri, type juggling con ==, impostazioni di sessione deboli, SSRF tramite wrapper URL, command injection e mass assignment. Le dipendenze Composer vulnerabili e le versioni di PHP non aggiornate, come i bug PHP2Shell, sono una causa di incidenti altrettanto comune.
Qual è il miglior scanner di sicurezza per PHP?
Nessuno strumento copre da solo la sicurezza PHP. Usi un analizzatore statico come PHPStan, Psalm o Semgrep per il codice, composer audit per le dipendenze Composer, uno scanner di container per l'immagine e un registro in fase di build di ciò che la CI ha davvero installato.
Come funziona composer audit e cosa non vede?
Confronta i pacchetti installati, o composer.lock con --locked, con gli advisory dell'API di Packagist.org, e segnala i pacchetti abbandonati e quelli contrassegnati come malware. Esce con 1 quando qualcosa non supera l'audit. Non vede PHP stesso, le estensioni PECL, i pacchetti del sistema operativo nell'immagine né nulla di ciò che la CI scarica fuori da Composer.
Come blocco i pacchetti Composer insicuri in CI?
Usi Composer 2.9 o successivo, che per impostazione predefinita blocca le versioni con advisory durante update e require (policy.advisories.block). Poiché una versione vulnerabile già nel lockfile viene comunque installata, esegua anche composer audit dopo composer install e faccia fallire il job in base al codice di uscita. Non usi mai --no-blocking in CI.
Laravel è sicuro per impostazione predefinita?
In gran parte sì. Blade fa l'escaping dell'output, Eloquent associa i parametri delle query, i modelli sono protetti dal mass assignment e l'ID di sessione viene rigenerato al login. Queste protezioni smettono di funzionare quando il codice usa {!! !!}, whereRaw() con input dell'utente, $guarded = [] o APP_DEBUG=true in produzione, oppure quando APP_KEY trapela.
Come aggiorno le dipendenze di un'app PHP legacy con pochi test?
Aggiorni il pacchetto a monte che porta dentro quello vulnerabile, un pacchetto alla volta, con composer update vendor/package --with-dependencies. Testi i percorsi critici, aggiunga test di caratterizzazione attorno a ciò che l'aggiornamento tocca e distribuisca per fasi.
Come genero uno SBOM per un progetto PHP?
Il plugin CycloneDX per Composer (cyclonedx/cyclonedx-php-composer) ne genera uno dai pacchetti Composer con composer CycloneDX:make-sbom. Elenca solo i pacchetti Composer. Uno SBOM in fase di build registra anche il runtime PHP, i pacchetti del sistema operativo e gli altri download della build.
Che cos'è PHP2Shell?
PHP2Shell è il nome di CVE-2026-109618 e CVE-2026-109619, due vulnerabilità critiche nello stesso PHP che possono consentire l'esecuzione di codice da remoto senza autenticazione. Sono corrette in PHP 8.5.12, 8.4.27, 8.3.36 e 8.2.35, rilasciate il 21 ottobre 2026. Veda PHP2Shell.
Veda ogni pacchetto che la sua build PHP installa
Esegua un workflow GitHub Actions su CRACI e confronti lo SBOM registrato con il suo composer.lock.
Prenota una demo