1. Zentrale Best Practices für sichere Serialisierung
Serialisierung ist wie das Verpacken von Gepäck am Flughafen: Wenn Sie nicht wissen, was drin ist und wem Sie den Koffer anvertrauen, kann es an der Sicherheitskontrolle eine böse Überraschung geben. In Java ermöglicht Serialisierung das einfache Speichern und Wiederherstellen von Objekten, öffnet aber zugleich die Tür für eine ganze Reihe von Angriffen, wenn die Daten aus unsicheren Quellen stammen.
Klassische Bedrohung:
Serialisierung in Java kann unsicher sein. Wenn ein Angreifer einen bösartigen Datenstrom unterschiebt, kann die Deserialisierung äußerst unangenehme Folgen haben: von der Änderung von Feldern bis hin zur Ausführung unerwünschten Codes. Das ist keine Schreckensgeschichte aus dem Lehrbuch – in der Java-Historie gab es tatsächlich Fälle, in denen Angriffe genau auf diesem Mechanismus basierten.
Warum ist das so?
Der Punkt ist: Deserialisierung ist nicht nur das Wiederherstellen von Feldwerten. Dabei wird ein vollwertiges Objekt erzeugt: Es können spezielle Methoden aufgerufen werden (z. B. readObject, readResolve), und mitunter – über Reflection – auch verwundbare Stellen im Code berührt werden. Besonders gefährlich sind Klassen aus Drittbibliotheken: Manche führen bereits während der Deserialisierung Aktionen aus. Vertrauen Sie daher niemals serialisierten Daten, die von außen stammen.
Verwenden Sie transient für sensible Daten
Wenn Ihre Klasse Felder enthält, die Passwörter, Tokens, private Schlüssel oder andere sensible Informationen speichern, deklarieren Sie sie als transient. Diese Daten gelangen nicht in den serialisierten Strom.
import java.io.Serializable;
public class User implements Serializable {
private String username;
private transient String password; // wird nicht serialisiert
// ...Konstruktoren, Getter, Setter...
}
Was passiert bei der Deserialisierung? Das Feld password hat den Standardwert (null für Strings). Das ist gut: Passwörter werden nicht in Dateien gespeichert bzw. über das Netzwerk übertragen.
Definieren Sie serialVersionUID explizit
Geben Sie serialVersionUID immer explizit an. Das reduziert die Wahrscheinlichkeit von Kompatibilitätsfehlern und minimiert das Risiko des Klassenersatzes bei der Deserialisierung.
private static final long serialVersionUID = 1L;
Warum ist das für die Sicherheit wichtig? Wenn Sie serialVersionUID nicht angeben, generiert der Compiler ihn automatisch anhand der Klassenstruktur. Das kann zu unerwarteten Abweichungen führen und theoretisch Missbrauch ermöglichen, wenn Klassen mit demselben Namen, aber anderer Struktur untergeschoben werden.
Prüfen Sie Objekttypen bei der Deserialisierung
Vertrauen Sie nicht blind dem, was aus dem Netzwerk oder aus einer Datei kommt. Prüfen Sie nach der Deserialisierung immer, ob das empfangene Objekt den erwarteten Typ hat, bevor Sie damit arbeiten.
Object obj = objectInputStream.readObject();
if (obj instanceof User) {
User user = (User) obj;
// sicher mit user arbeiten
} else {
// Unerwarteter Typ – Ausnahme werfen oder Fehler behandeln
}
Wozu ist das nötig? Ein bösartiger Datenstrom kann ein Objekt einer anderen Klasse enthalten, das zwar Serializable implementiert, aber nicht Ihrer Geschäftslogik entspricht.
Beschränken Sie die Klassen, die deserialisiert werden dürfen (ObjectInputFilter)
Seit Java 9 verwenden Sie Filter – ObjectInputFilter –, um die Menge der Klassen zu begrenzen, die deserialisiert werden dürfen. Das ist wie eine Einlasskontrolle am Eingang.
Beispiel: Filter setzen
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(); // jetzt werden nur User und Address deserialisiert
Dieser Filter erlaubt nur die Klassen User und Address Ihrer Anwendung. Alle anderen werden blockiert – es wird eine Ausnahme ausgelöst. Das reduziert das Risiko erheblich, dass ein bösartiges Objekt eingeschleust wird.
Deserialisieren Sie keine Daten aus unsicheren Quellen
Goldene Regel: Wenn Sie sich über die Quelle der Daten nicht sicher sind – nicht deserialisieren. Bevorzugen Sie Formate, die beim Parsen keinen Code ausführen (z. B. JSON, XML mit sicheren Parsern).
Beispiel für eine schlechte Praxis:
// Machen Sie das niemals mit Daten aus dem Internet!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // gefährlich!
Was ist besser?
- Verwenden Sie JSON-Parser (z. B. Gson/Jackson) oder XML-Parser mit Validierung.
- Wenn binäre Serialisierung nötig ist – filtern Sie Klassen via ObjectInputFilter und prüfen Sie die Typen (instanceof).
Verwenden Sie alternative Formate für den Austausch mit externen Systemen
Für Integrationen verwenden Sie Formate, die beim Parsen keinen Code ausführen: JSON, XML, Protocol Buffers u. a. Das schließt Angriffe über Deserialisierung nahezu aus.
// Verwenden Sie einen JSON-Parser statt ObjectInputStream
User user = gson.fromJson(jsonString, User.class);
Speichern Sie serialisierte Objekte nicht an öffentlich zugänglichen Orten
Dateien mit serialisierten Objekten können sensible Daten enthalten. Speichern Sie sie nicht in öffentlich zugänglichen Verzeichnissen und beschränken Sie die Zugriffsrechte auf Ebene des Dateisystems.
Verlassen Sie sich nicht auf Serialisierung zur Integritätsprüfung
Serialisierung garantiert weder Integrität noch Authentizität der Daten. Verwenden Sie digitale Signaturen, Prüfsummen oder Verschlüsselung, wenn Änderungen unzulässig sind.
2. Praxis: Beispiel für ObjectInputFilter und Demonstration einer Schwachstelle
Beispiel der Klassenfilterung
Angenommen, wir haben eine Klasse User:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password;
// ...Konstruktoren, Getter, Setter...
}
Der Filter erlaubt nur User:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;!*"
);
in.setObjectInputFilter(filter);
Wenn nun jemand versucht, ein Objekt einer anderen Klasse unterzuschieben, schlägt die Deserialisierung mit einem Fehler fehl.
Demonstration einer potenziellen Schwachstelle
Schädliche Klasse:
// Angenommen, jemand hat eine solche Klasse untergeschoben
public class Evil implements java.io.Serializable {
static {
System.out.println("Schädlicher Code ausgeführt!");
// hier könnte alles Mögliche stehen...
}
}
Wenn Sie Klassen nicht filtern, kann bei der Deserialisierung ein Objekt Evil erzeugt werden, und der statische Initialisierer wird beim Laden der Klasse ausgeführt – das ist bereits ein realer Angriff.
4. Typische Fehler bei der Absicherung der Serialisierung
Fehler Nr. 1: Deserialisierung ohne Filterung und Typprüfung. Häufig lesen Entwickler ein Objekt aus dem Strom und casten es sofort auf den gewünschten Typ. Das öffnet Tür und Tor für Angriffe. Verwenden Sie ObjectInputFilter und prüfen Sie den Typ mit instanceof.
Fehler Nr. 2: Speicherung sensibler Daten ohne transient. Wenn Sie vergessen, Passwörter/Schlüssel als transient zu deklarieren, landen sie im Strom und können zusammen mit der Datei abfließen.
Fehler Nr. 3: Fehlender serialVersionUID. Ohne expliziten serialVersionUID sind unerwartete Kompatibilitätsfehler und Risiken durch Klassenersatz möglich.
Fehler Nr. 4: Einsatz von Serialisierung für den Austausch mit externen Systemen. Binäre Serialisierung ist innerhalb einer Anwendung praktisch (z. B. Cache), aber für externen Austausch gefährlich. Bevorzugen Sie JSON/XML/Proto mit sicheren Parsern.
Fehler Nr. 5: Ignorieren der Datenintegrität. Eine Änderung der Bytes einer serialisierten Datei bleibt unbemerkt. Setzen Sie digitale Signaturen, Prüfsummen oder Verschlüsselung ein.
GO TO FULL VERSION