Neue Java-17-Funktionen für mehr Sicherheit und bessere Serialisierung
21. Oktober 2021
0 Min. LesezeitIm Dezember 2020 habe ich den Artikel Serialisierung und Deserialisierung in Java: Erläuterung der Java-Deserialisierungs-Schwachstelle über die Probleme mit der benutzerdefinierten Serialisierungsimplementierung von Java geschrieben. Das Serialisierungs-Framework ist so tief in Java eingebettet, dass es wichtig ist zu wissen, wie gefährlich manche Implementierungen sein können. Unsichere Deserialisierung kann zur Ausführung beliebigen Codes führen, wenn aus den Klassen in Ihrem Classpath eine Gadget-Kette erstellt wird.
Vor Kurzem wurde Java 17 veröffentlicht – die neue LTS-Version. Doch wie wirken sich die neuen Funktionen auf dieses Problem aus, und lassen sich Deserialisierungs-Schwachstellen damit besser verhindern?
In diesem Blogbeitrag beschäftige ich mich mit drei wichtigen Java-17-Funktionen:
Records
Verbesserungen am Java Flight Recorder (JFR)
JEP 415 (Java Enhancement Proposal): kontextspezifische Deserialisierungsfilter.
1. Records
Records wurden in Java 14 als Vorschaufunktion eingeführt und mit Java 16 allgemein verfügbar. Da viele Entwicklerinnen und Entwickler jedoch lieber nur auf LTS-Versionen aktualisieren, bietet es sich an, Records jetzt mit der vollständigen Veröffentlichung von Java 17 im Zusammenhang mit Serialisierung zu betrachten.
Im Gegensatz zu normalen POJOs wird beim Deserialisieren eines Records der Konstruktor verwendet, um das Objekt wiederherzustellen. Bei gewöhnlichen Java-Objekten ist das nicht der Fall; hier ist das Framework stark auf Reflection angewiesen. Das bedeutet, dass beim Deserialisieren eines gewöhnlichen Java-Objekts keine Logik ausgeführt wird, die durch den Konstruktor ausgelöst wird. Lesen Sie meinen vorherigen Blogbeitrag, um mehr zu erfahren. Bei Records kommt beim Wiederherstellen des Objekts während der Deserialisierung keine Magie zum Einsatz. Wenn Sie aus irgendeinem Grund Validierungslogik in den Konstruktor eines Records einfügen, wissen wir jetzt, dass diese Logik angewendet wird,
Dennoch lässt sich darüber diskutieren, ob Sie überhaupt Logik in einem Record unterbringen sollten. Eine falsche Umsetzung kann Gadgets erzeugen, die Teil einer Deserialisierungs-Gadget-Kette werden. Noch wichtiger ist, dass wir weiterhin die Funktion readObject() für die Deserialisierung verwenden. Das bedeutet, dass wir unabhängig von Records weiterhin anfällig für Gadget-Ketten in normalen POJOs sind.
2. Deserialisierungsfilter in Java
Um Deserialisierungs-Schwachstellen in Java zu begegnen, können Sie Serialisierungsfilter festlegen. Diese wurden in Java 9 mit der Implementierung von JEP 290 eingeführt. Sie können die Array-Größe, die Graphentiefe, die Gesamtzahl der Referenzen und die Stream-Größe begrenzen. Außerdem können Sie anhand eines Musters Blockier- und Zulassungslisten erstellen, um die Klassen einzuschränken, die deserialisiert werden dürfen.
Sie können einen solchen Filter als globalen JVM-Filter oder für jeden Stream einzeln festlegen. Für den globalen Filter können Sie ein JVM-Argument verwenden oder ihn im Code festlegen. Im folgenden Beispiel habe ich einen Filter erstellt, der alle Klassen aus mypackage zulässt und alle anderen blockiert.
JVM-Argument:
Code:
Sie können wie im folgenden Beispiel auch einen Filter für einen bestimmten Stream festlegen.
Bis einschließlich Java 17 wird bei einem Filter für einen bestimmten Stream der globale Filter für diesen Stream überschrieben. Der globale Filter wird also keineswegs mit dem streamspezifischen Filter kombiniert. Das ist wenig flexibel. Außerdem kann es dazu führen, dass Ihr globaler Filter nicht greift, wenn eine eingebundene Bibliothek die Deserialisierung für Sie übernimmt.
Kontextspezifische Deserialisierungsfilter in Java 17
Java 17 hat den Deserialisierungsfilter mit der Implementierung von JEP 415 erweitert.
Zu den wichtigsten Neuerungen gehört, dass Sie jetzt eine SerialFilterFactory für ObjectInputFilter.Config festlegen können. Diese Factory muss ein BinaryOperator sein und legt fest, was geschieht, wenn einem bestimmten Stream ein Filter hinzugefügt wird.
Im folgenden Beispiel weise ich Config eine einfache Factory zu, die zum Zusammenführen des bestehenden Filters mit dem neuen Filter die Standardmethode verwendet. Mit diesem Tool kann ich bestimmen, ob und wie Filter zusammengeführt werden. Damit wird das zuvor beschriebene Problem gelöst, das entsteht, wenn Ihre Bibliotheken den globalen Filter verarbeiten.
Neben der Filter Factory bietet Java 17 auch praktische Hilfsmethoden, mit denen sich Filter leicht erstellen lassen. Funktionen wie allowFilter() und rejectFilter() in ObjectInputFilter sind meiner Meinung nach eine deklarativere und leichter verständliche Möglichkeit, Filter zu erstellen.
Im folgenden Java-17-Codebeispiel verwende ich diese neuen Funktionen. In der Deserialisierungsmethode lehne ich die Gadget-Klasse ausdrücklich ab. Sowohl die Gadget-Klasse als auch der Record TwoValue gehören zum selben Package. Der Filter lehnt nun Gadget ab und lässt alle anderen Klassen in diesem Package zu.
3. Deserialisierungsereignisse im Java Flight Recorder
Mit der Veröffentlichung von Java 17 gibt es auch eine nützliche Erweiterung des Java Flight Recorder (JFR), die Sie im Kampf gegen Deserialisierungs-Exploits unterstützt. Diese Java-Version bietet jetzt ein bestimmtes Ereignis zur Überwachung der Deserialisierung. Für jedes Objekt in einem Stream wird ein Deserialisierungsereignis erstellt. Es erfasst allerlei interessante Informationen, etwa den tatsächlichen Typ, ob ein Filter vorhanden war, ob das Objekt gefiltert wurde, die Objekttiefe und die Anzahl der Referenzen.
Anhand all dieser Informationen können Sie feststellen, ob irgendwo in Ihrer Anwendung eine Deserialisierung stattfindet und was während der Ausführung Ihres Prozesses tatsächlich deserialisiert wird. Sie müssen dieses Ereignis allerdings aktivieren. Standardmäßig wird es nicht erfasst. Daher müssen Sie eine bestimmte Konfiguration vornehmen, ähnlich wie in diesem Beispiel.
Sehen wir uns das vorherige Codebeispiel an, diesmal jedoch mit der Deserialisierung der Gadget-Klasse. Wenn ich den Code in IntelliJ IDEA mit dem Java Flight Recorder und der benutzerdefinierten Konfiguration ausführe, erhalte ich folgendes Ergebnis.

Sie sehen, dass das Ereignis beim Deserialisieren den tatsächlichen Objekttyp erfasst und dass der Filter dieses Objekt ablehnt. Wenn Sie genauer erfahren möchten, wie Sie dieses bestimmte Deserialisierungsereignis im JFR verwenden, lesen Sie den Blogbeitrag Überwachung der Deserialisierung für mehr Anwendungssicherheit von Chris Hegarty.
Aktualisieren Sie auf Java 17 und nutzen Sie leistungsstärkere Schutzmaßnahmen gegen unsichere Deserialisierungs-Exploits
Die Java-17-LTS-Version bietet wesentliche Verbesserungen, um böswillige Deserialisierung in Ihren Java-Anwendungen zu verhindern. Wenn Sie diese Maßnahmen umsetzen möchten, sollten Sie daher unbedingt auf eine neuere Version wie Java 17 aktualisieren. Meiner Meinung nach sollten Sie die benutzerdefinierte Serialisierung von Java dennoch möglichst ganz vermeiden. Wenn Sie jedoch darauf angewiesen sind oder eine Bibliothek verwenden, die die benutzerdefinierte Deserialisierung von Java ausführt, wissen Sie jetzt, wie Sie sich schützen können.
Achten Sie außerdem darauf, keine Bibliotheken einzubinden, die bekannte Deserialisierungs-Gadget-Ketten oder andere Sicherheitsprobleme bei der Deserialisierung enthalten.
Starten Sie mit Capture the Flag
Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.



