In Java, Serialization
means converting Java objects into a byte stream; Deserialization
means converting the serialized object’s byte stream back to the original Java object.
1. Hello World Java Serialization
In Java, we have to implement the Serializable
interface to supports serialization and deserialization.
1.1 A Java object.
Person.java
package com.favtuts.io.object; import java.io.Serializable; import java.math.BigDecimal; public class Person implements Serializable { private String name; private int age; private BigDecimal salary; // getters setters constructor }
1.2 A complete Java Serialization and Deserialization example for a Java object Person.java
.
HelloSerialization.java
package com.favtuts.io.object; import java.io.*; import java.math.BigDecimal; public class HelloSerialization { public static void main(String[] args) { Person person = new Person("favtuts", 40, new BigDecimal(900)); byte[] bytes = convertObjectToBytes(person); Person p = (Person) convertBytesToObject(bytes); System.out.println(p); } // Convert object to byte[] public static byte[] convertObjectToBytes(Object obj) { ByteArrayOutputStream boas = new ByteArrayOutputStream(); try (ObjectOutputStream ois = new ObjectOutputStream(boas)) { ois.writeObject(obj); return boas.toByteArray(); } catch (IOException ioe) { ioe.printStackTrace(); } throw new RuntimeException(); } // Convert byte[] to object public static Object convertBytesToObject(byte[] bytes) { InputStream is = new ByteArrayInputStream(bytes); try (ObjectInputStream ois = new ObjectInputStream(is)) { return ois.readObject(); } catch (IOException | ClassNotFoundException ioe) { ioe.printStackTrace(); } throw new RuntimeException(); } }
Output
Person{name='favtuts', age=40, salary=900}
2. java.io.NotSerializableException
If we serialize an object which didn’t implement the Serializable
interface, Java throws java.io.NotSerializableException
.
Person.java
public class Person { private String name; private int age; private BigDecimal salary; // getters setters constructor }
HelloSerialization.java
Person person = new Person("favtuts", 40, new BigDecimal(900)); byte[] bytes = convertObjectToBytes(person);
Output
java.io.NotSerializableException: com.favtuts.io.object.Person
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1185)
at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:349)
at com.favtuts.io.object.HelloSerialization.convertObjectToBytes(HelloSerialization.java:23)
at com.favtuts.io.object.HelloSerialization.main(HelloSerialization.java:12)
Exception in thread "main" java.lang.RuntimeException
at com.favtuts.io.object.HelloSerialization.convertObjectToBytes(HelloSerialization.java:28)
at com.favtuts.io.object.HelloSerialization.main(HelloSerialization.java:12)
3. What is serialVersionUID?
If the serialVersionUID
is missing, the JVM will create it automatically. The serialVersionUID
is something like version number; in short, if we save an object with 1L
, we need to provide the same 1L
to read the object, else hits an incompatible error.
Person.java
public class Person implements Serializable { private static final long serialVersionUID = 1L; //... }
For example, we saved an object with serialVersionUID = 1L
into a file name person.obj
. Later we add or delete some fields from the object, and update serialVersionUID
to 2L
. Now, read the person.obj
file and try to convert it back to the modified object; since both serialVersionUID
is different, we will hit the following incompatible error:
Exception in thread "main" java.io.InvalidClassException: com.favtuts.io.object.Person;
local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:689)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1903)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1772)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2060)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
at com.favtuts.io.object.ObjectUtils.readObject(ObjectUtils.java:25)
at com.favtuts.io.object.ObjectUtils.main(ObjectUtils.java:38)
4. What is transient?
During serialization, JVM ignores all transient
fields. If we need to exclude specific object fields during serialization, mark them as transient.
Person.java
public class Person implements Serializable { private String name; private int age; //exclude this field private transient BigDecimal salary; // getters setters constructor }
Person person = new Person("favtuts", 40, new BigDecimal(900)); byte[] bytes = convertObjectToBytes(person); Person p = (Person) convertBytesToObject(bytes); System.out.println(p);
Output
Person{name='favtuts', age=40, salary=null}
5. Serialize Object to File
This example serializes a Java object to a file and deserializes the file back to the original object.
HelloSerializationFile.java
package com.favtuts.io.object; import java.io.*; import java.math.BigDecimal; public class HelloSerializationFile { public static void main(String[] args) throws IOException, ClassNotFoundException { Person person = new Person("favtuts", 50, new BigDecimal(1000)); File file = new File("person.anything"); writeObjectToFile(person, file); Person p = readObjectFromFile(file); System.out.println(p); } // Serialization // Save object into a file. public static void writeObjectToFile(Person obj, File file) throws IOException { try (FileOutputStream fos = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(fos)) { oos.writeObject(obj); oos.flush(); } } // Deserialization // Get object from a file. public static Person readObjectFromFile(File file) throws IOException, ClassNotFoundException { Person result = null; try (FileInputStream fis = new FileInputStream(file); ObjectInputStream ois = new ObjectInputStream(fis)) { result = (Person) ois.readObject(); } return result; } }
6. Why need Serialization in Java?
Note
Read this Brian Goetz – Towards Better Serialization.
Some use cases for Java serialization:
- A data-interchange format between sockets, applications, clients, and servers. It likes JSON or XML but in byte format.
- The serialization is the foundation of several critical Java’s technologies, including Java Remote Method Invocation (Java RMI), Common Object Request Broker Architecture (CORBA), and distributed computing such as Java Naming and Directory Interface (JNDI), Java Management Extensions (JMX) and Java Messaging (JMS).
- For gaming as lightweight persistence, we can serialize the current game’s state on disk and restore it later. The file is in byte format, and we can’t easily modify the sensitive game’s data like weapons or money.
- Saving an object graph disk for further analysis, like UML class diagram.
- Cluster of servers and restoring a session. For example, a cluster of servers (Server A and Server B) needs to synchronize the session if one of the servers is shut down or restart. When Server A is shutting down, it saves the objects as an attribute of the
HttpSession
and sends it over the network for Server B, and Server B can restore the objects from theHttpSession
. Read this post
P.S For data-interchange, consider human-readable data-interchange format like JSON, XML or Google protocol buffers.
7. Deserialization of Untrusted Data
However, deserialize from untrusted byte streams is dangerous and can result in the following Java vulnerabilities:
- Turtles All The Way Down – Target nested containers such as lists within a list.
- SerialDOS or Denial of Service (DoS)
- Pufferfish
- Remote code execution (RCE)
Further Reading
Evil Pickles: DoS attacks based on Object-Graph Engineering
OWASP – Deserialization of untrusted data
7.1 Deserialization and StackOverflow
Deserialize the below byte stream, and it throws StackOverflowError
.
StackOverflowExample.java
package com.favtuts.io.object.attack; import java.io.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; public class StackOverflowExample { public static void main(String[] args) { System.out.println(bomb().length); deserialize(bomb()); // throws StackOverflow System.out.println("Done"); } static byte[] bomb() { HashMap map = new HashMap(); List list = new ArrayList(); map.put(list, ""); list.add(list); return serialize(map); } public static byte[] serialize(Object o) { ByteArrayOutputStream ba = new ByteArrayOutputStream(); try { new ObjectOutputStream(ba).writeObject(o); } catch (IOException e) { throw new IllegalArgumentException(e); } return ba.toByteArray(); } public static Object deserialize(byte[] bytes) { try { return new ObjectInputStream( new ByteArrayInputStream(bytes)).readObject(); } catch (IOException | ClassNotFoundException e) { throw new IllegalArgumentException(e); } } }
Output
Exception in thread "main" java.lang.StackOverflowError
at java.base/java.util.ArrayList.hashCode(ArrayList.java:582)
at java.base/java.util.ArrayList.hashCodeRange(ArrayList.java:595)
//...
7.2 Deserialization and Denial-of-service attack (DoS attack)
Deserializing the below byte stream will keep the process running, hanging there, and slowly slowing down the system, a classic DoS attack.
DosExample.java
package com.favtuts.io.object.attack; import java.io.*; import java.util.HashSet; import java.util.Set; public class DosExample { public static void main(String[] args) throws Exception { System.out.println(bomb().length); deserialize(bomb()); // Dos here System.out.println("Done"); } static byte[] bomb() { Set<Object> root = new HashSet<>(); Set<Object> s1 = root; Set<Object> s2 = new HashSet<>(); for (int i = 0; i < 100; i++) { Set<Object> t1 = new HashSet<>(); Set<Object> t2 = new HashSet<>(); t1.add("test-" + i); // make it not equal to t2 s1.add(t1); // root also add set s1.add(t2); s2.add(t1); s2.add(t2); s1 = t1; // reference to t1, so that `root` can add new set from the last t1 s2 = t2; } return serialize(root); } public static byte[] serialize(Object o) { ByteArrayOutputStream ba = new ByteArrayOutputStream(); try { new ObjectOutputStream(ba).writeObject(o); } catch (IOException e) { throw new IllegalArgumentException(e); } return ba.toByteArray(); } public static Object deserialize(byte[] bytes) { try { return new ObjectInputStream( new ByteArrayInputStream(bytes)).readObject(); } catch (IOException | ClassNotFoundException e) { throw new IllegalArgumentException(e); } } }
Output
// keep the process running, hanging there
// and slowly slowing down the system, a classic DoS attack.
8. Java 9 Deserialization filters
Java 9, JEP 290 introduced deserialization filters ObjectInputFilter
to filter the incoming serialization data.
8.1 Deserialization Filter for StackOverflow
Refactor the above StackOverflowExample.java
example with a deserialization filter of maxdepth=2
. If we rerun the program, the deserialization process will stop and return filter status: REJECTED
.
StackOverflowExample.java
package com.favtuts.io.object.attack; import java.io.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; public class StackOverflowExample { public static void main(String[] args) { System.out.println(bomb().length); //deserialize(bomb()); // throws StackOverflow ObjectInputFilter filter = ObjectInputFilter.Config.createFilter( "maxdepth=2;java.base/*;!*"); // java.io.InvalidClassException: filter status: REJECTED deserializeFilter(bomb(), filter); System.out.println("Done"); } static byte[] bomb() { HashMap map = new HashMap(); List list = new ArrayList(); map.put(list, ""); list.add(list); return serialize(map); } public static byte[] serialize(Object o) { ByteArrayOutputStream ba = new ByteArrayOutputStream(); try { new ObjectOutputStream(ba).writeObject(o); } catch (IOException e) { throw new IllegalArgumentException(e); } return ba.toByteArray(); } public static Object deserializeFilter(byte[] bytes, ObjectInputFilter filter) { try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bais)) { // add filter before readObject ois.setObjectInputFilter(filter); return ois.readObject(); } catch (IOException | ClassNotFoundException e) { throw new IllegalArgumentException(e); } } }
Output
Exception in thread "main" java.lang.IllegalArgumentException: java.io.InvalidClassException: filter status: REJECTED
at com.favtuts.io.object.attack.StackOverflowExample.deserializeFilter(StackOverflowExample.java:70)
at com.favtuts.io.object.attack.StackOverflowExample.main(StackOverflowExample.java:27)
Caused by: java.io.InvalidClassException: filter status: REJECTED
8.2 Deserialization Filter for DoS attack
Refactor the above DosExample.java
example with maxdepth
deserialization filter to stop the DoS attack.
DosExample.java
package com.favtuts.io.object.attack; import java.io.*; import java.util.HashSet; import java.util.Set; public class DosExample { public static void main(String[] args) throws Exception { System.out.println(bomb().length); //deserialize(bomb()); // Dos here ObjectInputFilter filter = ObjectInputFilter.Config.createFilter( "maxdepth=10;java.base/*;!*"); deserializeFilter(bomb(), filter); // Dos here System.out.println("Done"); } static byte[] bomb() { Set<Object> root = new HashSet<>(); Set<Object> s1 = root; Set<Object> s2 = new HashSet<>(); for (int i = 0; i < 100; i++) { Set<Object> t1 = new HashSet<>(); Set<Object> t2 = new HashSet<>(); t1.add("test-" + i); // make it not equal to t2 s1.add(t1); // root also add set s1.add(t2); s2.add(t1); s2.add(t2); s1 = t1; // reference to t1, so that `root` can add new set from the last t1 s2 = t2; } return serialize(root); } public static byte[] serialize(Object o) { ByteArrayOutputStream ba = new ByteArrayOutputStream(); try { new ObjectOutputStream(ba).writeObject(o); } catch (IOException e) { throw new IllegalArgumentException(e); } return ba.toByteArray(); } public static Object deserializeFilter(byte[] bytes, ObjectInputFilter filter) { try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bais)) { // add filter before readObject ois.setObjectInputFilter(filter); return ois.readObject(); } catch (IOException | ClassNotFoundException e) { throw new IllegalArgumentException(e); } } }
If we rerun the program, the deserialization process will stop and returns filter status: REJECTED
.
Exception in thread "main" java.lang.IllegalArgumentException:
java.io.InvalidClassException: filter status: REJECTED
at com.favtuts.io.object.attack.DosExample.deserializeFilter(DosExample.java:74)
at com.favtuts.io.object.attack.DosExample.main(DosExample.java:19)
Note
Read the official guide on Serialization Filtering.
9. Java 17 Context-Specific Deserialization Filters
Java 17 JEP 415 introduced a filter factory, which allows choosing different deserialization filters dynamically or context-specific, refer to the Java 17 filter factory examples.
Download Source Code
$ git clone https://github.com/favtuts/java-core-tutorials-examples
$ cd java-io/object
References
- Java version history
- Serialization Filtering
- OWASP – Deserialization of untrusted data
- Brian Goetz – Towards Better Serialization
- Evil Pickles: DoS attacks based on Object-Graph Engineering
- Java Object Serialization Specification
- Serializable JavaDoc
- ObjectInputStream JavaDoc
- ObjectOutputStreamJavaDoc
- Java – What is serialVersionUID
- JEP 290: Filter Incoming Serialization Data
- What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability.
- Java Serialization vulnerability