Skip to main content

Neue Java-17-Funktionen für mehr Sicherheit und bessere Serialisierung

Artikel von
Blog Feature Java deserialize

21. Oktober 2021

0 Min. Lesezeit

Im 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:

  1. Records

  2. Verbesserungen am Java Flight Recorder (JFR)

  3. 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:

-Djdk.serialFilter=nl.brianvermeer.example.*;!*

Code:

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("nl.brianvermeer.example.*;!*");
ObjectInputFilter.Config.setSerialFilter(filter);

Sie können wie im folgenden Beispiel auch einen Filter für einen bestimmten Stream festlegen.

ObjectInputStream in = new ObjectInputStream(fileIn);
ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("nl.brianvermeer.example2.Object;!*");
in.setObjectInputFilter(filesOnlyFilter);

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.

ObjectInputFilter.Config.setSerialFilterFactory((f1, f2) -> ObjectInputFilter.merge(f2,f1));

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.

public static void main(String[] args) throws IOException, ClassNotFoundException {
   var filename = "file.ser";
   var value = new TwoValue("one", "two");
   //var value = new Gadget(new Command("ls -l")); //This will not be deserialized

   var filter1 = ObjectInputFilter.allowFilter(cl -> cl.getPackageName().contentEquals("nl.brianvermeer.example.serialize.records"), ObjectInputFilter.Status.REJECTED);
   ObjectInputFilter.Config.setSerialFilter(filter1);
   ObjectInputFilter.Config.setSerialFilterFactory((f1, f2) -> ObjectInputFilter.merge(f2,f1));

   serialize(value, filename);
   deserialize(filename);
}

public static void serialize(Object value, String filename) throws IOException {
   System.out.println("---serialize");
   FileOutputStream fileOut = new FileOutputStream(filename);
   ObjectOutputStream out = new ObjectOutputStream(fileOut);
   out.writeObject(value);
   out.close();
   fileOut.close();
}

public static void deserialize(String filename) throws IOException, ClassNotFoundException {
   System.out.println("---deserialize");
   FileInputStream fileIn = new FileInputStream(filename);
   ObjectInputStream in = new ObjectInputStream(fileIn);
   ObjectInputFilter intFilter = ObjectInputFilter.rejectFilter(cl -> cl.equals(Gadget.class), ObjectInputFilter.Status.UNDECIDED);
   in.setObjectInputFilter(intFilter);
   TwoValue tv = (TwoValue) in.readObject();
   System.out.println(tv);

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.

<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0" description="test">
   <event name="jdk.Deserialization">
      <setting name="enabled">true</setting>
      <setting name="stackTrace">false</setting>
   </event>
</configuration>

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.

Ereignisansicht des Java-Profilers mit einem abgelehnten Deserialisierungsereignis und seinen Eigenschaften, darunter Typ, gelesene Bytes und Filterstatus.

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.

Weiterlesen

Blog

Frontier-Modelle fanden die Schwachstellen. Nur der Angreifer fand die Angriffsketten.

Die statische Analyse fand die Schwachstellen, doch erst Live-Angriffstests bewiesen, wie sie sich zu Angriffen verketten lassen. Ein Vergleich von Evo COS, Claude Security und Claude Code Security.

feature insights context
Blog

Autonome Angriffe sind bereits Realität. Die Verteidigung muss Schritt halten.

Autonome Angreifer verkürzen das Zeitfenster für die Verteidigung. Erfahren Sie, wie kontinuierliches Erkennen, Beheben, Validieren und Verhindern Sicherheitsteams helfen kann, Schritt zu halten.

Blog

Warum KI-Coding-Agenten immer wieder fehlerhafte Zugriffskontrollen schreiben

KI-Coding-Agenten können Autorisierungslogik erzeugen, die kompiliert und die Prüfung besteht, dabei aber die Daten eines Mandanten für einen anderen offenlegt. Erfahren Sie, warum fehlerhafte Zugriffskontrollen schwer zu erkennen sind und wie Sie sie verhindern.