Context
Three critical advisories published 2026-03-03 (GHSA-g38g-8gr9-h9xp, GHSA-vvpj-8cmc-gx39, GHSA-7wx9-6375-f5wh) were fixed in v1.0.4 by adding the reported vectors to _unsafe_globals. The patch is correct and the release notes are clear — this is not a complaint about the response.
This issue tracks the architectural recommendation from GHSA-vvpj-8cmc-gx39 that wasn't addressed by the fix: the blocklist approach is structurally bypassable, and the pkgutil.resolve_name fix doesn't close the class of attack it represents.
The remaining bypass class
GHSA-vvpj-8cmc-gx39 was a universal bypass: one unblocked function (pkgutil.resolve_name) could reach every blocked function. The fix adds pkgutil.resolve_name to the blocklist. But the same indirection pattern is available through at least:
importlib.import_module — importlib.import_module('os').system returns os.system without a direct GLOBAL opcode for os
importlib.util.find_spec + importlib.util.module_from_spec chain — loads a module without import_module
operator.attrgetter on a module object already on the stack from a prior REDUCE — partially blocked but the chained form is still reachable
The pattern: any function that takes a string and returns a callable, where the callable isn't itself a GLOBAL opcode, is invisible to the current scanner. pkgutil.resolve_name was the most elegant instance; it's not the only one.
The fix the advisory recommended
From GHSA-vvpj-8cmc-gx39:
Consider switching to an allowlist (default-deny) for permitted globals. Treat ALL unknown globals as dangerous by default (currently marked 'Suspicious' but not counted as issues).
The _safe_globals dict already exists in scanner.py. The current logic is:
- If in
_unsafe_globals → DANGEROUS
- If in
_safe_globals → INNOCUOUS
- Otherwise → SUSPICIOUS (not counted as an issue)
Flipping step 3 to DANGEROUS would be a breaking change for legitimate use cases (custom model classes, third-party serializers), but it would close the entire bypass class. The _safe_globals allowlist is already maintained — the question is whether unknown globals should be trusted by default.
Comparison: fickling's approach
fickling (Trail of Bits) decompiles the pickle into Python AST and analyzes what the code does rather than what it imports. A chained resolve_name('os:system')('id') is visible as a function call on an opaque return value, which fickling flags regardless of the import chain. The tradeoff is complexity and false positive rate; the benefit is that indirection chains don't evade it.
picklescan's opcode-level approach has real value — especially for the HuggingFace use case where scan latency matters. But the current _safe_globals infrastructure suggests the maintainer has already thought about this boundary.
Concrete asks
Not requesting an immediate rewrite. Three smaller asks:
- Track
importlib as the next bypass candidate — importlib.import_module is the most obvious resolve_name equivalent not yet blocked
- Consider a
--strict mode that promotes SUSPICIOUS → DANGEROUS, for users who can enumerate their legitimate globals
- Document the trust model — the README implies picklescan is a reliable safety gate. A note that 'CLEAN means no known dangerous globals were found, not that the file is safe to load' would set accurate expectations for downstream users treating CLEAN scans as guarantees
Context
Three critical advisories published 2026-03-03 (GHSA-g38g-8gr9-h9xp, GHSA-vvpj-8cmc-gx39, GHSA-7wx9-6375-f5wh) were fixed in v1.0.4 by adding the reported vectors to
_unsafe_globals. The patch is correct and the release notes are clear — this is not a complaint about the response.This issue tracks the architectural recommendation from GHSA-vvpj-8cmc-gx39 that wasn't addressed by the fix: the blocklist approach is structurally bypassable, and the
pkgutil.resolve_namefix doesn't close the class of attack it represents.The remaining bypass class
GHSA-vvpj-8cmc-gx39 was a universal bypass: one unblocked function (
pkgutil.resolve_name) could reach every blocked function. The fix addspkgutil.resolve_nameto the blocklist. But the same indirection pattern is available through at least:importlib.import_module—importlib.import_module('os').systemreturnsos.systemwithout a direct GLOBAL opcode forosimportlib.util.find_spec+importlib.util.module_from_specchain — loads a module withoutimport_moduleoperator.attrgetteron a module object already on the stack from a prior REDUCE — partially blocked but the chained form is still reachableThe pattern: any function that takes a string and returns a callable, where the callable isn't itself a GLOBAL opcode, is invisible to the current scanner.
pkgutil.resolve_namewas the most elegant instance; it's not the only one.The fix the advisory recommended
From GHSA-vvpj-8cmc-gx39:
The
_safe_globalsdict already exists in scanner.py. The current logic is:_unsafe_globals→ DANGEROUS_safe_globals→ INNOCUOUSFlipping step 3 to DANGEROUS would be a breaking change for legitimate use cases (custom model classes, third-party serializers), but it would close the entire bypass class. The
_safe_globalsallowlist is already maintained — the question is whether unknown globals should be trusted by default.Comparison: fickling's approach
fickling (Trail of Bits) decompiles the pickle into Python AST and analyzes what the code does rather than what it imports. A chained
resolve_name('os:system')('id')is visible as a function call on an opaque return value, which fickling flags regardless of the import chain. The tradeoff is complexity and false positive rate; the benefit is that indirection chains don't evade it.picklescan's opcode-level approach has real value — especially for the HuggingFace use case where scan latency matters. But the current
_safe_globalsinfrastructure suggests the maintainer has already thought about this boundary.Concrete asks
Not requesting an immediate rewrite. Three smaller asks:
importlibas the next bypass candidate —importlib.import_moduleis the most obviousresolve_nameequivalent not yet blocked--strictmode that promotes SUSPICIOUS → DANGEROUS, for users who can enumerate their legitimate globals