1. Sicurezza della serializzazione binaria
La serializzazione in Java — non è soltanto il salvataggio dei campi di un oggetto. È la possibilità di «ricostruire» qualsiasi oggetto con qualsiasi contenuto, purché implementi l'interfaccia Serializable. Sembra comodo! Ma se la tua applicazione deserializza dati provenienti da una fonte non affidabile (ad esempio dalla rete o da un file che un attaccante potrebbe aver manomesso), rischia di diventare vittima di attacchi.
Come funziona?
Durante la deserializzazione Java crea oggetti a partire dal flusso di byte, senza chiamare i costruttori delle classi. Se nella classe sono definiti metodi speciali come readObject, verranno chiamati automaticamente. Un attaccante può «costruire» un flusso di byte tale da eseguire codice vulnerabile al momento della deserializzazione.
Esempio: attacco «gadget chain»
Immagina di avere una classe che durante la deserializzazione avvia un comando esterno (legge un file o invoca la shell). Se l'attaccante sa che l'applicazione deserializza oggetti di determinati tipi, può fornire un flusso di byte appositamente preparato che porta all'esecuzione di codice malevolo. Tali attacchi sono costruiti come una «gadget chain» — una sequenza di chiamate che si conclude con un'operazione pericolosa.
Perché è così grave?
La deserializzazione — è un processo in cui dal flusso di dati vengono ripristinati oggetti reali, e in quel momento può essere eseguito codice arbitrario. Se i dati provengono da una fonte non affidabile, si apre la porta all'esecuzione di comandi da remoto (RCE). Grandi aziende hanno pubblicato raccomandazioni per abbandonare la deserializzazione non sicura; nei progetti enterprise moderni la serializzazione binaria è spesso vietata dalle policy di sicurezza.
Come proteggersi?
- Non deserializzare mai oggetti da fonti non affidabili.
- Usa il whitelisting — un elenco esplicito di tipi ammessi alla deserializzazione.
- Preferisci formati testuali per le integrazioni esterne: JSON, XML.
- Se la serializzazione è inevitabile, utilizza librerie con impostazioni di deserializzazione sicura (ad esempio, Jackson con restrizioni sui tipi).
- Limita l'uso di metodi di serializzazione non standard (readObject, readResolve ecc.), se non sei certo della loro sicurezza.
2. Compatibilità tra versioni di classe
La serializzazione binaria in Java è strettamente legata alla struttura della classe. Hai serializzato un oggetto nella versione 1.0, poi hai aggiornato la classe (aggiunto/rimosso un campo) — il tentativo di deserializzare l'«oggetto vecchio» nella nuova versione può portare a un errore o alla perdita di dati.
Come Java determina la compatibilità?
A questo scopo viene usato un campo speciale — serialVersionUID. È un identificatore di versione della classe. Se l'oggetto serializzato ha un serialVersionUID e la classe corrente ne ha un altro — verrà lanciata InvalidClassException, e la deserializzazione non avrà luogo.
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L; // versione specificata esplicitamente
private String name;
private int age;
}
Se modifichi la struttura della classe (ad esempio, aggiungendo il campo email) e non cambi serialVersionUID, Java considererà la classe compatibile e proverà a deserializzare l'oggetto vecchio. Se invece non hai specificato serialVersionUID esplicitamente, la JVM lo genererà automaticamente sulla base della struttura, e qualsiasi modifica porterà a incompatibilità.
Cosa succede in caso di mancata/avvenuta corrispondenza delle versioni?
Se gli identificatori non coincidono — la deserializzazione non avverrà: InvalidClassException. Se coincidono — i campi vengono associati per nome e tipo: i campi nuovi riceveranno i valori predefiniti (null, 0), quelli rimossi — vengono ignorati. In caso di modifica del tipo o del nome di un campo sono possibili errori e un'interpretazione scorretta dei dati.
Consiglio pratico. Nelle classi serializzabili specifica sempre un serialVersionUID esplicito. Modificalo solo in caso di cambiamenti incompatibili (rimozione/cambio di tipo di un campo importante). Quando aggiungi nuovi campi l'identificatore può rimanere invariato — la JVM gestirà correttamente gli oggetti vecchi.
Tabella: Cosa accade quando si modifica la classe
| Modifica nella classe | Cosa succede durante la deserializzazione? |
|---|---|
| Aggiunta di un nuovo campo | Riceve il valore predefinito (0, null) |
| Rimozione di un campo | Ignorato durante la lettura dei dati vecchi |
| Modifica del tipo di campo | Eccezione o dati non corretti |
| Modifica del nome del campo | Il campo vecchio viene ignorato, il nuovo ha il valore predefinito |
| Modifica di serialVersionUID | Eccezione InvalidClassException |
3. Limitazioni della serializzazione standard
Non tutti gli oggetti sono serializzabili
I campi con i modificatori transient e static non vengono serializzati. static — perché appartiene alla classe, non all'oggetto; transient — perché hai esplicitamente vietato la serializzazione del campo.
Alcuni oggetti per definizione non sono serializzabili: Thread, connessioni al DB, socket, Scanner ecc. Se la tua classe ha un campo di questo tipo e non è transient, otterrai NotSerializableException.
import java.io.Serializable;
import java.util.Scanner;
public class Session implements Serializable {
private transient Scanner scanner; // non viene serializzato!
private String login;
}
Problemi di prestazioni ed estensibilità
La serializzazione di grandi grafi di oggetti può essere lenta e richiedere molta memoria.
Il formato binario è poco adatto alle integrazioni con altre piattaforme e linguaggi — lo «capisce» solo Java.
È difficile controllare cosa venga effettivamente serializzato, soprattutto con gerarchie profonde e riferimenti ciclici.
Problemi con il supporto di dati legacy
La conservazione a lungo termine di snapshot binari — è rischiosa. Dopo uno o due anni la struttura delle classi cambia e i vecchi file smettono di caricarsi.
Storia reale: «Abbiamo salvato una cache degli utenti serializzata tre anni fa, abbiamo aggiornato l'applicazione, e ora non riusciamo a caricarla. Addio, dati persi!»
4. Best practice: come evitare le trappole
- Usa la serializzazione binaria solo per attività interne, dove controlli entrambe le parti del processo.
- Non usare la serializzazione binaria per integrazioni esterne e per l'archiviazione a lungo termine di dati importanti.
- Specifica sempre serialVersionUID in modo esplicito nelle classi serializzabili.
- Contrassegna con il modificatore transient i campi che non devono essere serializzati.
- Per lo scambio con sistemi esterni — formati testuali e librerie moderne: JSON, XML, Jackson, Gson, JAXB.
- Per la compatibilità usa il versioning: conserva la versione dell'oggetto nella stessa classe e adatta l'elaborazione in fase di deserializzazione.
- Se la serializzazione serve solo per la cache — non mantenere la compatibilità a tutti i costi: la cache è più semplice da ricalcolare.
- Non memorizzare negli oggetti serializzabili dati sensibili (password, chiavi) — la serializzazione non cifra i dati.
5. Errori tipici nell'uso della serializzazione binaria
Errore n. 1: Deserializzazione di dati da una fonte non affidabile. L'errore più pericoloso — accettare e deserializzare oggetti arrivati «dalla strada» (dalla rete, dall'utente, da un file manomesso). È una via diretta a vulnerabilità fino all'RCE.
Errore n. 2: Modifica implicita della struttura della classe senza aggiornare serialVersionUID. Se non specifichi l'identificatore esplicitamente, la JVM lo genererà automaticamente. Qualsiasi cambiamento della struttura (anche l'ordine dei campi) porterà a incompatibilità e all'impossibilità di caricare gli oggetti vecchi.
Errore n. 3: Tentare di serializzare oggetti con campi non serializzabili. Se una classe ha un campo che non implementa Serializable e non è transient, la serializzazione terminerà con un'eccezione.
Errore n. 4: Memorizzare negli oggetti serializzabili dati temporanei o sensibili. Token, password, descrittori temporanei di risorse — tutto ciò può finire accidentalmente in un file.
Errore n. 5: Uso della serializzazione binaria per l'archiviazione a lungo termine e per lo scambio tra versioni. Dopo il primo aggiornamento delle classi è alto il rischio di dati «corrotti» e problemi di compatibilità.
Errore n. 6: Aspettarsi che static e i campi transient «si ripristinino» dopo la deserializzazione. Questi campi non vengono serializzati; dopo il caricamento avranno i valori predefiniti.
GO TO FULL VERSION