In breve: per la sicurezza di unserialize in PHP vale una regola semplice: non elaborare mai con unserialize() dati che provengono dall'esterno. Usi invece JSON. Dove unserialize() deve restare per via di codice legacy, lo limiti con allowed_classes => false e verifichi l'autenticità dei dati con una firma.
Che cosa può andare storto con unserialize()
PHP può trasformare array e oggetti in una stringa con serialize() e ripristinarli con unserialize(). È comodo per salvare dati in cookie, cache o campi del database. Il problema: unserialize() crea così anche oggetti di classi arbitrarie che possono essere caricate nell'applicazione.
Alla creazione e al più tardi alla distruzione di un simile oggetto, PHP richiama automaticamente determinati metodi, ad esempio __wakeup(), __unserialize() o __destruct(). Se classi del progetto o di librerie contengono tali metodi con azioni su file, database o sistema, un aggressore può usarle in modo improprio tramite dati manipolati. Gli esperti parlano di PHP Object Injection. A seconda delle classi presenti, l'effetto va dalla cancellazione di singoli file all'esecuzione di codice dannoso.
In sintesi per i non tecnici: una sola chiamata sconsiderata nel punto sbagliato può lasciare entrare un aggressore in profondità nell'applicazione. Questa debolezza rientra perciò da anni tra i temi della OWASP Top 10.
L'alternativa sicura: JSON invece di unserialize
Per quasi tutti i casi in cui i dati arrivano dall'esterno o lasciano il server, JSON è la scelta giusta. json_decode() crea solo array, numeri, stringhe e valori booleani, non oggetti di classi proprie.
<?php
declare(strict_types=1);
function leseEinstellungen(string $eingabe): array
{
try {
$daten = json_decode($eingabe, true, 16, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new InvalidArgumentException('Ungültige Daten.');
}
if (!is_array($daten)) {
throw new InvalidArgumentException('Erwartet wurde ein Objekt.');
}
// Accettare solo i campi noti con i tipi attesi
return [
'sprache' => in_array($daten['sprache'] ?? '', ['de', 'en'], true) ? $daten['sprache'] : 'de',
'seiten' => max(1, min(100, (int) ($daten['seiten'] ?? 20))),
];
}
È importante la seconda parte: anche i dati JSON sono input. Accetti solo i campi attesi e verifichi tipi e intervalli di valori prima di lavorarci.
Quando unserialize è inevitabile: allowed_classes e firma
Nelle applicazioni più vecchie i dati serializzati si trovano spesso in database o cache che non si possono convertire subito. Allora limiti il rischio in due passi. Primo, impedisca con allowed_classes la creazione di oggetti. Secondo, garantisca con una firma HMAC che i dati provengano davvero dalla Sua applicazione e non siano stati modificati lungo il percorso.
<?php
declare(strict_types=1);
const SCHLUESSEL = 'aus-sicherer-konfiguration-laden'; // mai nel repository del codice
function verpacken(array $daten): string
{
$roh = serialize($daten);
return hash_hmac('sha256', $roh, SCHLUESSEL) . '.' . base64_encode($roh);
}
function auspacken(string $paket): array
{
[$signatur, $b64] = array_pad(explode('.', $paket, 2), 2, '');
$roh = base64_decode($b64, true);
if ($roh === false || !hash_equals(hash_hmac('sha256', $roh, SCHLUESSEL), $signatur)) {
throw new RuntimeException('Signatur ungültig.');
}
$daten = unserialize($roh, ['allowed_classes' => false, 'max_depth' => 16]);
if (!is_array($daten)) {
throw new RuntimeException('Unerwartetes Format.');
}
return $daten;
}
Se servono comunque determinate classi, le indichi una per una in allowed_classes, invece di consentirle tutte. La chiave va nella configurazione del server o in un archivio di segreti e deve essere rinnovata dopo un incidente.
Convertire gradualmente i dati esistenti a JSON
Nelle applicazioni cresciute negli anni i valori serializzati restano spesso per anni nel database. Una conversione in un solo passo è allora rischiosa. Si è dimostrato valido un passaggio graduale: l'applicazione legge entrambi i formati, ma scrive solo più JSON. Alla lettura di un vecchio valore, questo viene decompresso una sola volta con allowed_classes => false e subito riscritto come JSON. Dopo un po' la maggior parte delle voci è convertita e uno script una tantum si occupa del resto.
Durante il passaggio registri quanto spesso viene ancora letto il vecchio formato e anche quando le firme non corrispondono. Tali fallimenti possono indicare tentativi di manipolazione e rientrano nel monitoraggio. Solo quando non compare più alcun vecchio formato, rimuova del tutto dal codice il ramo con unserialize().
Deserializzazione nascosta: sessioni, cache e percorsi di file
Non ogni deserializzazione compare in modo visibile nel codice proprio. Controlli anche questi punti:
- Sessioni: PHP serializza automaticamente i dati di sessione. Finché gli aggressori non possono scrivere i file di sessione, non è critico. Percorsi di sessione separati per ogni sito sono descritti nella guida Mettere in sicurezza le sessioni in PHP.
- Serializzatore delle sessioni: l'avviso su CVE-2026-115560 riguarda una configurazione insolita in cui il
serialize_handlerviene cambiato durante l'esecuzione conini_set()e l'upload progress delle sessioni è attivo. Misura secondo php.net:session.upload_progress.enabled=0nel php.ini o nel pool FPM, poi riavvio dei worker. - Cache e code: Redis, Memcached o tabelle di database con valori serializzati sono sicuri quanto l'accesso a essi. Non vanno esposti apertamente in rete.
- Funzioni sui file con percorsi da input: non passi mai alle funzioni sui file percorsi presi dalla richiesta. Gli stream wrapper come
phar://possono altrimenti innescare elaborazioni inattese. Un elenco di consentiti per i nomi dei file protegge qui in modo più affidabile di qualsiasi filtraggio. - Dipendenze: le librerie con debolezze note ampliano le possibilità di un aggressore. Le tenga aggiornate, come descritto nella guida Verificare le vulnerabilità delle dipendenze Composer.
Trovare i punti a rischio nel codice
Un primo inventario riesce con una semplice ricerca. Non modifica nulla nel sistema e mostra tutte le chiamate al di fuori della directory vendor:
grep -rn --include='*.php' 'unserialize(' /var/www/projekt \
| grep -v '/vendor/'
Valuti ogni riscontro con tre domande: da dove vengono i dati? Un utente può modificarli? allowed_classes è impostato? I riscontri con input da cookie, $_GET, $_POST, $_REQUEST o interfacce esterne hanno la precedenza. Li sostituisca con JSON e provi la conversione prima di rilasciarla.
Un ulteriore indizio si trova nel browser. I dati PHP serializzati hanno una struttura tipica con identificatori di tipo e indicazioni di lunghezza. Se trova tali valori in cookie, campi nascosti dei moduli o parametri del Suo sito, l'applicazione ha consegnato dati serializzati al visitatore e presumibilmente se li aspetta di ritorno. Proprio questi punti vanno controllati.
Per la direzione basta una semplice domanda a sviluppatori o agenzia: «La nostra applicazione elabora da qualche parte dati serializzati che provengono dall'esterno e, in caso affermativo, come sono protetti?» Una risposta chiara con i punti in cui si trovano è un buon segno. Una risposta evasiva è un motivo per far verificare l'applicazione.
Come aiuta concretamente PHP SOS?
- Cerchiamo nel Suo codice la deserializzazione con dati estranei e valutiamo ogni ritrovamento per raggiungibilità e impatto.
- Proponiamo passaggi concreti a JSON o chiamate protette e affianchiamo i Suoi sviluppatori nell'attuazione.
- Verifichiamo le dipendenze alla ricerca di debolezze note e documentiamo lo stato.
- Su richiesta facciamo verificare l'applicazione in modo indipendente nell'ambito di un audit di sicurezza e penetration test.
Domande frequenti
unserialize() in PHP è insicuro per principio?
Non per dati che genera solo la Sua applicazione e che nessuno può modificare. Diventa pericoloso non appena utenti o terzi possono influenzare l'input, ad esempio tramite cookie, campi di moduli o interfacce esterne.
allowed_classes => false basta come protezione?
Impedisce la creazione di oggetti arbitrari e chiude così la via d'attacco più importante. Per input dall'esterno JSON resta comunque la scelta migliore, perché il parser è più semplice e non ricostruisce oggetti.
Un aggiornamento delle librerie aiuta contro gli attacchi di deserializzazione?
In parte. Gli attacchi sfruttano spesso classi delle dipendenze che eseguono azioni quando un oggetto viene distrutto o risvegliato. I pacchetti aggiornati chiudono le catene note, ma la vera causa è la chiamata a unserialize con dati estranei.
PHP2Shell ha qualcosa a che fare con la deserializzazione?
Le due vulnerabilità principali si trovano nell'elaborazione delle richieste di PHP. L'avviso su CVE-2026-115560 riguarda invece una configurazione insolita della serializzazione delle sessioni. La misura lì è session.upload_progress.enabled=0.