XStream deserialization attack explained

XStream deserialization attack explained

Written by

in

An xstream deserialization attack shows that the danger is never the data format. XStream is a popular Java library that converts objects to XML and back. Its job is to read an XML document and rebuild the Java object it describes. The catch is that the XML gets to say which classes to build, and by default XStream will build almost anything. Point it at a document that names the right classes and reconstructing the object graph runs attacker chosen code. This post walks through the mechanism and the type permission fix.

How xstream deserialization turns XML into objects

XStream maps XML element names to Java classes and element contents to fields. A harmless document round trips a simple object:

<appConfig>
  <name>acme-notes</name>
  <replicas>3</replicas>
</appConfig>

To rebuild that, XStream reads the root element, resolves it to a class, creates an instance, and populates the fields. So far so good. The trouble is that the element name, and optional class attributes, decide the type. An attacker who controls the XML controls which classes XStream instantiates and how their fields are set. If this is your first look at rebuilding objects from untrusted input, start with our primer on what insecure deserialization is and the hub on what a deserialization gadget chain is.

Type coercion is the whole attack

Because the document chooses types, an attacker writes XML that builds a sequence of objects whose construction reaches a dangerous action. XStream can map into dynamic proxies, handler objects, and collection types whose setup logic, triggered during deserialization, invokes methods the attacker picked. The XML is longer than a native serialized blob, but the idea is identical to the Commons Collections gadget chain: assemble real classes into a chain, then let the deserializer fire it.

<!-- Conceptual shape, not a working payload -->
<dynamic-proxy>
  <interface>some.Interface</interface>
  <handler class="a.chain.of.transformers">
    <!-- steps that reach a method call during construction -->
  </handler>
</dynamic-proxy>

XStream does exactly what you asked: it reads a description of an object and builds it. The flaw is trusting the description, because the description came from an attacker and it named classes you never meant to allow.

The gadget classes that finish the job are whatever useful types sit on the classpath, the same population that powers the other chains in this cluster. XStream is just a different doorway to them. For how a usable sequence of classes is discovered, see finding deserialization gadget chains.

Where untrusted XML reaches XStream

As with the YAML case, teams assume XML parsing is internal and therefore safe. It often is not.

  • An endpoint that accepts XML request bodies and calls xstream.fromXML on them.
  • A SOAP style service or a legacy integration that exchanges XStream encoded objects.
  • An import or restore feature that reads an XStream document a user uploaded.
  • A queue or cache whose contents are not fully trusted.

Every one of those is a path from attacker input to fromXML, which is the sink. The question is the same one we ask of every parser: could these bytes come from someone outside my trust boundary?

How to fix xstream deserialization with type permissions

Modern XStream ships a security framework built on type permissions. The idea is an allowlist: deny everything, then permit only the specific types your app actually deserializes. Newer versions default to a deny posture, but you should set the allowlist explicitly so you are not relying on a default that a version change could alter.

XStream xstream = new XStream();
// Start from nothing allowed
xstream.addPermission(NoTypePermission.NONE);
// Permit only the types you expect
xstream.allowTypes(new Class[] { AppConfig.class });
AppConfig config = (AppConfig) xstream.fromXML(untrustedInput);
  • Deny by default, then allow named types. This is the single most effective control. If a document names anything outside your allowlist, XStream refuses to build it.
  • Avoid allowing broad wildcards. Permitting whole packages or any type by interface quietly reopens the hole. Keep the list to concrete classes.
  • Prefer a data only format where you can. If the integration does not truly need arbitrary object graphs, a typed JSON or a schema validated XML mapping removes the open ended construction entirely.
  • Keep XStream current. Updates remove known dangerous mappings and strengthen the default posture. Treat the allowlist as your real defense and the version as backup.

One principle across the whole cluster

Type permission allowlisting in XStream, SafeConstructor in SnakeYAML deserialization, and ObjectInputFilter in Java RMI and JRMP deserialization are the same fix wearing different names. Each one takes away the attacker’s ability to name arbitrary types and hands that decision back to you. The format, XML, YAML, native bytes, or an RMI stream, is only the envelope. The payload is always the same: untrusted input choosing which classes come to life.

This is an injection class bug, untrusted input reaching a sink it was never meant to touch. For more in that family, browse our injection and input category.

Catching it means following an XML body from the request edge to the exact fromXML call and checking whether a type permission allowlist stands between the two. Reasoning from source to sink about untrusted input reaching a deserializer is exactly what UnboundCompute is built to do. More on our about page.

Frequently asked questions

What is an XStream deserialization attack?

XStream converts XML back into Java objects, and the XML document decides which classes to build. By default XStream will instantiate almost any type named in the document, so an attacker who controls the XML can build a chain of classes whose construction reaches attacker chosen code.

How is this different from native Java deserialization?

The mechanism is the same, only the envelope differs. Instead of a serialized byte blob, the attacker writes an XML document that names the classes, often through dynamic proxies or handler objects. The gadget classes on the classpath that finish the job are the same population used by other chains.

Where does untrusted XML reach XStream in a real app?

Through endpoints that accept XML request bodies, SOAP style or legacy integrations that exchange XStream objects, import or restore features that read uploaded documents, and queues or caches whose contents are not fully trusted. Any of these can reach a fromXML call.

How do I prevent XStream deserialization attacks?

Use XStream type permissions as an allowlist: deny everything with NoTypePermission.NONE, then permit only the concrete classes you expect. Avoid broad wildcard permissions, prefer a data only format where arbitrary object graphs are not needed, and keep XStream current as backup to the allowlist.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.