Skip to content
Injection Reviewed 2026-09-12

Insecure Deserialization

What does this mean ?

Deserialization becomes dangerous when untrusted bytes can select types, invoke object hooks or rebuild executable behavior. Native object formats such as Python pickle or Java serialization are different from parsing a small JSON object into expected fields. Not every use of JSON is safe: permissive polymorphic type loading, unbounded input and later unsafe use can reintroduce risk.

What can happen ?

Consequences can include code execution, resource exhaustion, unexpected state changes or altered privileges. Checking the returned object's type after deserialization may be too late because hooks may already have run.

Recommendation

Use data-only formats with an explicit schema. Bound the HTTP body before parsing, enforce expected keys and types, and construct the application's object yourself. Do not accept a class name from the payload. Keep authorization decisions such as roles and tenant identity out of client-controlled fields.

HTML escaping does not make object deserialization safe. A signature establishes provenance only if keys and the producer are trusted; it does not remove dangerous behavior from the format. For unavoidable legacy formats, use narrowly configured filters, resource limits and isolation while planning migration. Microsoft states that BinaryFormatter cannot be made secure for untrusted data.

Sample Code

These fragments show how to accept a small display-name object. They are alternatives, not sequential operations. Apply request-size limits before allocating the full body. Each application still needs authentication, authorization and safe output rendering.

# Unsafe for bytes from an untrusted source
profile = pickle.loads(raw_bytes)
import json

def parse_profile(raw: str) -> dict[str, str]:
    if len(raw) > 4096:
        raise ValueError("Profile too large")
    data = json.loads(raw)
    if not isinstance(data, dict) or set(data) != {"displayName"}:
        raise ValueError("Unexpected profile fields")
    name = data["displayName"]
    if not isinstance(name, str) or not 1 <= len(name) <= 80:
        raise ValueError("Invalid display name")
    return {"displayName": name}

Do not pass a cookie through node-serialize, eval or a function reviver. In TypeScript, treat the result of parsing as unknown until narrowed.

function parseProfile(raw) {
  if (typeof raw !== 'string' || raw.length > 4096) {
    throw new Error('Invalid profile size');
  }
  const value = JSON.parse(raw);
  if (value === null || typeof value !== 'object' || Array.isArray(value) ||
      Object.keys(value).length !== 1 ||
      !Object.hasOwn(value, 'displayName') ||
      typeof value.displayName !== 'string' ||
      value.displayName.length < 1 || value.displayName.length > 80) {
    throw new Error('Invalid profile');
  }
  return { displayName: value.displayName };
}

Returning a newly constructed object avoids merging arbitrary client keys into application configuration.

// Unsafe for untrusted data
$profile = unserialize($raw);
function parseProfile(string $raw): array {
    if (strlen($raw) > 4096) {
        throw new InvalidArgumentException('Profile too large');
    }
    $value = json_decode($raw, true, 8, JSON_THROW_ON_ERROR);
    if (!is_array($value) || array_keys($value) !== ['displayName'] ||
        !is_string($value['displayName']) ||
        strlen($value['displayName']) < 1 || strlen($value['displayName']) > 320) {
        throw new InvalidArgumentException('Invalid profile');
    }
    return ['displayName' => $value['displayName']];
}

PHP's byte-length check here is a resource bound, not a Unicode character-count policy. allowed_classes does not turn untrusted unserialize into a recommended interchange format.

.NET 8+ System.Text.Json, using a fixed DTO with no attacker-selected polymorphic type:

public sealed record Profile(string DisplayName);

static Profile ParseProfile(string raw)
{
    if (raw.Length > 4096) throw new ArgumentException("Profile too large");
    var options = new System.Text.Json.JsonSerializerOptions
    {
        MaxDepth = 8,
        UnmappedMemberHandling =
            System.Text.Json.Serialization.JsonUnmappedMemberHandling.Disallow
    };
    var profile = System.Text.Json.JsonSerializer.Deserialize<Profile>(raw, options);
    if (profile is null || string.IsNullOrWhiteSpace(profile.DisplayName) ||
        profile.DisplayName.Length > 80)
        throw new ArgumentException("Invalid profile");
    return profile;
}

Avoid replacing this with BinaryFormatter or unrestricted type-name handling. Duplicate-key behavior should be specified if the contract or downstream systems require it.

Prefer a fixed DTO with a data parser such as Jackson, with no default typing enabled. This Java 17+ fragment assumes a fresh ObjectMapper and bounded input:

record Profile(String displayName) {}

static Profile parseProfile(String raw) throws java.io.IOException {
    if (raw.length() > 4096) throw new IllegalArgumentException("Profile too large");
    var mapper = new com.fasterxml.jackson.databind.ObjectMapper();
    mapper.enable(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    Profile profile = mapper.readValue(raw, Profile.class);
    if (profile == null || profile.displayName() == null ||
            profile.displayName().isBlank() || profile.displayName().length() > 80) {
        throw new IllegalArgumentException("Invalid profile");
    }
    return profile;
}

An ObjectInputFilter can constrain unavoidable native Java serialization, but requires a reviewed class and resource policy. A type check after readObject() is not equivalent.

For YAML input, explicitly restrict Psych's safe loader rather than using an unsafe object loader:

require 'yaml'

def parse_profile(raw)
  raise ArgumentError, 'Profile too large' if raw.bytesize > 4096
  value = YAML.safe_load(raw, permitted_classes: [], permitted_symbols: [], aliases: false)
  unless value.is_a?(Hash) && value.keys == ['displayName'] &&
         value['displayName'].is_a?(String) && (1..80).cover?(value['displayName'].length)
    raise ArgumentError, 'Invalid profile'
  end
  { 'displayName' => value['displayName'] }
end

Do not use Marshal.load or YAML.unsafe_load for this input. YAML loader defaults differ across versions; explicit options make the intended policy visible.

Regression checks

Test a valid profile, missing field, extra role field, null, array, numeric name, overlong body and nested object. Confirm invalid input becomes a controlled client error and never reaches business logic. For legacy object formats, use harmless fixture classes whose hooks record a local marker; denied classes must be rejected before their hooks run. Avoid real command or network side effects.

References