Skip to content

fix: safe_pickle safe_dump executes __reduce__ on untrusted objects during serialization without type validation #1506

@AlexFiliakov

Description

@AlexFiliakov

Problem Statement

The safe_pickle.py module's safe_dump function (lines 225-243) serializes objects using pickle.dumps with HIGHEST_PROTOCOL and then signs the result with HMAC. However, the serialization step itself (pickle.dumps) can trigger arbitrary code execution via the __reduce__, __reduce_ex__, __getstate__, or __getnewargs__ methods of the object being serialized. If a maliciously-constructed object (e.g., from user-supplied config or untrusted deserialization) is passed to safe_dump, these dunder methods execute during serialization, BEFORE any integrity signing occurs.

This is distinct from the deserialization vulnerability (RestrictedUnpickler addresses that). This is a serialization-time vulnerability where the act of pickling an object can trigger code execution.

v1.0 Impact

Medium -- this requires an attacker to first get a malicious object into the application state, which is a prerequisite that may already constitute a compromise. However, if the application processes user-supplied objects (e.g., from imported scenarios or configs that construct custom objects), this could be exploited. This should be fixed before v1.0 as defense-in-depth.

Affected Code

  • ergodic_insurance/safe_pickle.py:L239 -- data = pickle.dumps(obj, protocol=protocol)
  • ergodic_insurance/safe_pickle.py:L293 -- data = pickle.dumps(obj, protocol=protocol) in safe_dumps

Current Behavior

def safe_dump(obj, f, protocol=pickle.HIGHEST_PROTOCOL, key_dir=None):
    data = pickle.dumps(obj, protocol=protocol)  # __reduce__ executes here
    key = _get_or_create_hmac_key(key_dir)
    signature = hmac.new(key, data, hashlib.sha256).digest()
    f.write(signature)
    f.write(data)

If obj has a malicious __reduce__ method (e.g., def __reduce__(self): import os; os.system("whoami"); return (str, ("safe",))), the pickle.dumps call executes __reduce__ which runs the malicious code.

Attack scenario: An application imports scenario configurations that construct custom objects, or a deserialized object (that passed RestrictedUnpickler) has been crafted to have a malicious __reduce__ that only triggers on re-serialization.

Expected Behavior

safe_dump should validate the type of obj against an allowlist of known-safe types before serialization. Alternatively, use pickletools.dis to inspect the pickle stream for dangerous opcodes before writing.

Alternative Solutions Evaluated

  1. Type-check obj before serialization: Verify type(obj) is in an allowlist of known-safe project types. Pros: Simple, prevents arbitrary object serialization. Cons: May be overly restrictive for container types with nested objects.
  2. Use copyreg to control serialization: Register custom pickle reducers for project types that avoid executing user-controlled code. Pros: Fine-grained control. Cons: High maintenance.
  3. Document the risk: Add a warning that only trusted objects should be passed to safe_dump. Pros: Low effort. Cons: Not a technical mitigation.

Recommended Approach

Option 1: Add a type allowlist check at the top of safe_dump. For container types (dict, list, tuple), recursively check contained elements. This is defense-in-depth -- the primary mitigation is ensuring untrusted objects don't enter the application state.

Acceptance Criteria

  • safe_dump validates object type before serialization
  • Type allowlist documented and configurable
  • Warning emitted if object type is not in allowlist
  • All existing tests continue to pass
  • New security test: verify malicious reduce is caught before serialization

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority-mediumMedium prioritysecuritySecurity vulnerabilities and hardening

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions