Skip to content

security: do not auto-select code-executing formats (pickle, dill) from a file extension - #14

Open
hackchang wants to merge 1 commit into
hgrecco:masterfrom
hackchang:fix/no-code-exec-autoroute
Open

security: do not auto-select code-executing formats (pickle, dill) from a file extension#14
hackchang wants to merge 1 commit into
hgrecco:masterfrom
hackchang:fix/no-code-exec-autoroute

Conversation

@hackchang

Copy link
Copy Markdown

PR: security — don't auto-select code-executing formats (pickle, dill) from a file extension

What this fixes

serialize.load(path) / serialize.dump(obj, path) pick the backend purely from the file extension
(_get_format_from_ext). Because pickle and dill are registered with the .pickle / .dill extensions,
a caller who only intends to read data (serialize.load(user_path)) is silently routed to
pickle.Unpickler(...).load() / dill.Unpickler(...).load() — which execute arbitrary code — whenever the
path/extension is attacker-influenced. (CWE-502, Deserialization of Untrusted Data.)

This implements exactly the "explicit flag" hardening we discussed: code-executing backends are marked
unsafe=True and are never auto-selected from an extension; a caller must pass fmt= explicitly to use
them. Data formats (json, yaml, msgpack, …) are unchanged and keep auto-detecting.

Changes

  • all.py: new UNSAFE_FORMATS set; register_format(..., unsafe=False); _get_format_from_ext raises a
    clear ValueError (telling the caller to pass fmt=) if the extension maps to an unsafe format.
  • pickle.py, dill.py: register_format(..., unsafe=True).
  • testsuite/test_basic.py: the extension/auto-detection tests now assert the new behavior for unsafe formats.
  • testsuite/test_security.py: new regression tests (pickle/dill refused by extension, explicit fmt= still
    works, safe formats still auto-detect).

serialize.load(path, fmt="pickle") still works for the trusted-input case.

Test suite: unchanged pass/fail vs main on test_basic.py (the pre-existing test_round_trip failures are
unrelated), plus the new test_security.py passes.

Note on the YAML backend (the second vector in the report — your call on design)

The report also covered the yaml backends: serialize/yaml.py and serialize/yaml_legacy.py load with the
full, unsafe yaml.Loader, so a .yaml document containing !!python/object/apply:os.system [...] executes
code. I deliberately left the yaml change out of this PR because a clean fix needs a design decision that's
yours to make (you mentioned wanting a better API here). What I found while testing:

  • yaml.FullLoader blocks the RCE tags (!!python/object/apply / new / module) while still supporting
    !!python/tuple etc. yaml_legacy works perfectly under FullLoader (all round-trips pass, RCE blocked).
  • The main yaml backend serializes registered classes as !!python/object:... (the default dumper path),
    which FullLoader/SafeLoader reject — so making it safe means changing how custom types are encoded
    (e.g. emit only the SERIALIZED_TAG mapping + a safe dumper), which is a wire-format decision.
  • SafeLoader is fully safe but drops the ability to serialize arbitrary unregistered objects via yaml (that
    capability is what makes the RCE possible); only classes registered with register_class would round-trip.

Happy to send a follow-up PR for the yaml backend in whichever direction you prefer (FullLoader now + a safe
dumper later, or a stricter SafeLoader + register_class-only path). Just let me know.


Reported/authored by hackchang.

…om a file extension

serialize.load(path) / dump(obj, path) chose the backend purely from the file
extension, so a .pickle/.dill path was silently routed to pickle/dill (which
execute code on load). A caller that only meant to read data could reach
Unpickler.load() if the path/extension was attacker-influenced (CWE-502).

Mark such formats unsafe (register_format(..., unsafe=True)); they are never
auto-selected from an extension and require an explicit fmt=. Data formats are
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hackchang
hackchang force-pushed the fix/no-code-exec-autoroute branch from 9c97569 to fa0ce21 Compare August 13, 2026 01:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant