1. Best practice fondamentali per una serializzazione sicura
La serializzazione è come preparare i bagagli in aeroporto: se non sai cosa c’è dentro e a chi affidi la valigia, potresti avere brutte sorprese ai controlli di sicurezza. In Java la serializzazione consente di salvare e ripristinare facilmente gli oggetti, ma apre anche la porta a un intero spettro di attacchi quando i dati provengono da fonti non affidabili.
Minaccia classica:
La serializzazione in Java può essere non sicura. Se un attaccante invia un flusso malevolo, durante la deserializzazione possono verificarsi conseguenze spiacevoli: dalla modifica dei campi all’esecuzione di codice indesiderato. Non è una storiella da manuale — nella storia di Java ci sono stati davvero casi in cui gli attacchi si basavano proprio su questo meccanismo.
Perché succede?
Il punto è che la deserializzazione non è solo il ripristino dei valori dei campi. Nel processo viene creato un oggetto completo: possono essere chiamati metodi speciali (ad esempio, readObject, readResolve) e talvolta — si attivano punti vulnerabili del codice tramite reflection. Sono particolarmente pericolose le classi di librerie di terze parti: alcune eseguono azioni già in fase di deserializzazione. Perciò non fidarti mai dei dati serializzati ricevuti dall’esterno.
Usa transient per i dati sensibili
Se nella tua classe ci sono campi che contengono password, token, chiavi private o altre informazioni sensibili, dichiarali come transient. Questi dati non finiranno nel flusso serializzato.
import java.io.Serializable;
public class User implements Serializable {
private String username;
private transient String password; // non viene serializzato
// ...costruttori, getter, setter...
}
Cosa succede in deserializzazione? Il campo password avrà il valore predefinito (null per le stringhe). È un bene: le password non verranno archiviate in file o inviate sulla rete.
Definisci esplicitamente serialVersionUID
Specifica sempre esplicitamente serialVersionUID. Questo riduce la probabilità di errori di compatibilità e minimizza il rischio di sostituzione delle classi durante la deserializzazione.
private static final long serialVersionUID = 1L;
Perché è importante per la sicurezza? Se non specifichi serialVersionUID, il compilatore lo genererà automaticamente in base alla struttura della classe. Ciò può portare a incongruenze inaspettate e, in teoria, ad abusi con la sostituzione di classi aventi lo stesso nome ma struttura diversa.
Verifica i tipi degli oggetti durante la deserializzazione
Non fidarti di ciò che arriva dalla rete o da un file. Dopo la deserializzazione controlla sempre che l’oggetto ottenuto abbia il tipo atteso prima di lavorarci.
Object obj = objectInputStream.readObject();
if (obj instanceof User) {
User user = (User) obj;
// è sicuro lavorare con user
} else {
// tipo imprevisto — lanciare un'eccezione o gestire l'errore
}
A cosa serve? Un flusso malevolo può contenere un oggetto di un’altra classe che implementa Serializable, ma non corrisponde alla tua logica di business.
Limita le classi che possono essere deserializzate (ObjectInputFilter)
A partire da Java 9, utilizza i filtri — ObjectInputFilter — per limitare l’insieme di classi consentite alla deserializzazione. È come un controllo all’ingresso.
Esempio: impostazione del filtro
import java.io.*;
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;com.example.Address;!*"
);
ObjectInputStream in = new ObjectInputStream(inputStream);
in.setObjectInputFilter(filter);
Object obj = in.readObject(); // adesso vengono deserializzati solo User e Address
Questo filtro consente solo le classi User e Address della tua applicazione. Tutte le altre verranno bloccate — verrà lanciata un’eccezione. Questo riduce sensibilmente il rischio di far entrare un oggetto malevolo.
Non deserializzare dati provenienti da fonti non affidabili
Regola d’oro: se non sei sicuro della fonte dei dati — non deserializzare. Preferisci formati che non eseguono codice durante il parsing (ad esempio, JSON, XML con parser sicuri).
Esempio di cattiva pratica:
// Non fatelo mai con dati da Internet!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // pericoloso!
Cosa fare meglio?
- Usare parser JSON (ad esempio Gson/Jackson) oppure parser XML con validazione.
- Se la serializzazione binaria è necessaria — filtrate le classi tramite ObjectInputFilter e verificate i tipi (instanceof).
Usa formati alternativi per lo scambio con sistemi esterni
Per le integrazioni, utilizza formati che non eseguono codice in fase di parsing: JSON, XML, Protocol Buffers, ecc. Questo quasi elimina gli attacchi via deserializzazione.
// Invece di ObjectInputStream usate un parser JSON
User user = gson.fromJson(jsonString, User.class);
Non archiviare oggetti serializzati in luoghi accessibili a tutti
I file con oggetti serializzati possono contenere dati sensibili. Non conservarli in directory pubbliche e limita i permessi a livello di file system.
Non fare affidamento sulla serializzazione per il controllo dell'integrità
La serializzazione non garantisce integrità o autenticità dei dati. Usa firme digitali, checksum o cifratura quando le modifiche non sono ammissibili.
2. Pratica: esempio di ObjectInputFilter e dimostrazione di una vulnerabilità
Esempio di filtraggio delle classi
Supponiamo di avere la classe User:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password;
// ...costruttori, getter, setter...
}
Il filtro consente solo User:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;!*"
);
in.setObjectInputFilter(filter);
Ora, se qualcuno prova a inserire un oggetto di un’altra classe, la deserializzazione terminerà con un errore.
Dimostrazione di una potenziale vulnerabilità
Classe dannosa:
// Immaginiamo che qualcuno abbia infilato una classe del genere
public class Evil implements java.io.Serializable {
static {
System.out.println("Codice dannoso eseguito!");
// qui potrebbe esserci di tutto...
}
}
Se non filtri le classi, durante la deserializzazione potrebbe essere creato un oggetto Evil, e l’inizializzatore statico verrà eseguito al caricamento della classe — questa è già un’attacco reale.
4. Errori tipici nella messa in sicurezza della serializzazione
Errore n. 1: deserializzazione senza filtraggio e verifica del tipo. Spesso gli sviluppatori leggono un oggetto dal flusso e lo castano subito al tipo richiesto. Questo apre la porta agli attacchi. Usa ObjectInputFilter e verifica il tipo con instanceof.
Errore n. 2: memorizzare dati sensibili senza transient. Se dimentichi di dichiarare password/chiavi come transient, finiranno nel flusso e potrebbero fuoriuscire insieme al file.
Errore n. 3: assenza di serialVersionUID. Senza un serialVersionUID esplicito sono possibili errori di compatibilità inattesi e rischi legati alla sostituzione delle classi.
Errore n. 4: usare la serializzazione per lo scambio con sistemi esterni. La serializzazione binaria è comoda all’interno dell’applicazione (ad esempio, cache), ma è rischiosa per lo scambio esterno. Preferisci JSON/XML/Proto con parser sicuri.
Errore n. 5: ignorare l’integrità dei dati. La modifica dei byte in un file serializzato passerà inosservata. Applica firme digitali, checksum o cifratura.
GO TO FULL VERSION