Skip to content

security: replace pickle.loads with RestrictedUnpickler (CWE-502)#691

Open
taylorodell wants to merge 2 commits into
mongodb:mainfrom
taylorodell:fix/restricted-unpickler-cwe502
Open

security: replace pickle.loads with RestrictedUnpickler (CWE-502)#691
taylorodell wants to merge 2 commits into
mongodb:mainfrom
taylorodell:fix/restricted-unpickler-cwe502

Conversation

@taylorodell

@taylorodell taylorodell commented May 29, 2026

Copy link
Copy Markdown

Ticket

N/A — External security contribution (CWE-502 remediation)

Notes

Problem: All pickle.loads() / pickle.load() calls in the deserialization path accept arbitrary types, enabling remote code execution via __reduce__ gadgets in crafted cache files.

Two input paths reach the vulnerable deserialization:

  1. Local filesystem.snooty-*.cache.gz files in the project directory
  2. HTTP download — when local cache is missing, snooty fetches from cache_url_prefix (S3 bucket in rstspec.toml) and deserializes the response

Fix: Introduces snooty/safe_unpickler.py with a SafeUnpickler that restricts deserialized types to a known allowlist:

  • Allowed: snooty.*, docutils.*, and standard builtins (dict, list, tuple, str, int, etc.)
  • Blocked: Everything else — os.system, subprocess.Popen, builtins.eval, and all standard pickle RCE gadget chains

All four pickle.loads() call sites are replaced with safe_loads():

  • parse_cache.py read_from_bytes() (line 140)
  • parse_cache.py CacheData.get() (line 90)
  • gizaparser/domain.py (line 95)
  • page_database.py (line 199)

Write-side pickle.dumps() calls are unchanged.

Tests added in snooty/test_safe_unpickler.py:

  • Blocks os.system gadgets ✓
  • Blocks subprocess gadgets ✓
  • Allows builtin types (dict, list, str) ✓
  • Allows snooty CacheData type ✓
  • Works with gzip-compressed payloads ✓

References:

README updates

  • This PR does not introduce changes that should be reflected in the README.md and/or HACKING.md

Replace all pickle.loads() / pickle.load() calls in the deserialization
path with a SafeUnpickler that restricts deserialized types to a known
allowlist of snooty, docutils, and builtin types.

This prevents arbitrary code execution via __reduce__ gadgets in
malicious cache files, whether delivered via:
  - Local filesystem (e.g., committed in a PR to a docs repo)
  - HTTP download from the cache_url_prefix S3 bucket

The SafeUnpickler blocks types like os.system, subprocess.Popen,
and any other module outside snooty/docutils — the standard pickle
RCE gadget chains all require importing from os, subprocess, or
similar modules.

Write-side pickle.dumps() calls are unchanged — they only serialize
known snooty types.

CWE-502: Deserialization of Untrusted Data
Ref: https://docs.python.org/3/library/pickle.html#restricting-globals
- Move safe_unpickler imports to the local imports section (isort compliance)
- Add test_safe_unpickler.py with tests for:
  - Blocking os.system gadgets
  - Blocking subprocess gadgets
  - Allowing builtin types
  - Allowing snooty CacheData types
  - Gzip-compressed payload handling
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