Fickling has Static Analysis Bypass via Incomplete Dangerous Module Blocklist
Description
Fickling is a Python pickling decompiler and static analyzer. Prior to version 0.1.7, the unsafe_imports() method in Fickling's static analyzer fails to flag several high-risk Python modules that can be used for arbitrary code execution. Malicious pickles importing these modules will not be detected as unsafe, allowing attackers to bypass Fickling's primary static safety checks. This issue has been patched in version 0.1.7.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ficklingPyPI | < 0.1.7 | 0.1.7 |
Affected products
1- Range: master, v0.0.1, v0.0.2, …
Patches
529d5545e74b0Add importlib, code and multiprocessing to unsafe modules
2 files changed · +129 −0
fickling/fickle.py+3 −0 modified@@ -882,6 +882,9 @@ def unsafe_imports(self) -> Iterator[ast.Import | ast.ImportFrom]: "cProfile", "ctypes", "pydoc", + "importlib", + "code", + "multiprocessing", ): yield node elif "eval" in (n.name for n in node.names):
test/test_bypasses.py+126 −0 modified@@ -222,3 +222,129 @@ def test_missing_pydoc(self): res.detailed_results()["AnalysisResult"].get("UnsafeImports"), "from pydoc import locate", ) + + # https://github.com/trailofbits/fickling/security/advisories/GHSA-q5qq-mvfm-j35x + def test_missing_importlib(self): + pickled = Pickled( + [ + op.Proto.create(5), + op.ShortBinUnicode("builtins"), + op.Memoize(), + op.ShortBinUnicode("getattr"), + op.Memoize(), + op.StackGlobal(), + op.Memoize(), + op.ShortBinUnicode("importlib"), + op.Memoize(), + op.ShortBinUnicode("import_module"), + op.Memoize(), + op.StackGlobal(), + op.Memoize(), + op.ShortBinUnicode("os"), + op.Memoize(), + op.TupleOne(), + op.Memoize(), + op.Reduce(), + op.Memoize(), + op.ShortBinUnicode("system"), + op.Memoize(), + op.TupleTwo(), + op.Memoize(), + op.Reduce(), + op.Memoize(), + op.ShortBinUnicode("id"), + op.Memoize(), + op.TupleOne(), + op.Memoize(), + op.Reduce(), + op.Memoize(), + op.Stop(), + ] + ) + res = check_safety(pickled) + self.assertGreater(res.severity, Severity.LIKELY_SAFE) + self.assertEqual( + res.detailed_results()["AnalysisResult"].get("UnsafeImports"), + "from importlib import import_module", + ) + + # https://github.com/trailofbits/fickling/security/advisories/GHSA-q5qq-mvfm-j35x + def test_missing_code(self): + pickled = Pickled( + [ + op.Proto.create(5), + op.ShortBinUnicode("builtins"), + op.Memoize(), + op.ShortBinUnicode("getattr"), + op.Memoize(), + op.StackGlobal(), + op.Memoize(), + op.ShortBinUnicode("code"), + op.Memoize(), + op.ShortBinUnicode("InteractiveInterpreter"), + op.Memoize(), + op.StackGlobal(), + op.Memoize(), + op.EmptyTuple(), + op.Memoize(), + op.Reduce(), + op.Memoize(), + op.ShortBinUnicode("runsource"), + op.Memoize(), + op.TupleTwo(), + op.Memoize(), + op.Reduce(), + op.Memoize(), + op.ShortBinUnicode('import os; os.system("id")'), + op.Memoize(), + op.TupleOne(), + op.Memoize(), + op.Reduce(), + op.Memoize(), + op.Stop(), + ] + ) + res = check_safety(pickled) + self.assertGreater(res.severity, Severity.LIKELY_SAFE) + self.assertEqual( + res.detailed_results()["AnalysisResult"].get("UnsafeImports"), + "from code import InteractiveInterpreter", + ) + + # https://github.com/trailofbits/fickling/security/advisories/GHSA-q5qq-mvfm-j35x + def test_missing_multiprocessing(self): + pickled = Pickled( + [ + op.Proto.create(5), + op.Frame(74), + op.ShortBinUnicode("multiprocessing.util"), + op.Memoize(), + op.ShortBinUnicode("spawnv_passfds"), + op.Memoize(), + op.StackGlobal(), + op.Memoize(), + op.ShortBinBytes(b"/bin/sh"), + op.Memoize(), + op.EmptyList(), + op.Memoize(), + op.Mark(), + op.BinGet(3), + op.ShortBinBytes(b"-c"), + op.Memoize(), + op.ShortBinBytes(b"id"), + op.Memoize(), + op.Appends(), + op.EmptyTuple(), + op.TupleThree(), + op.Memoize(), + op.Reduce(), + op.Memoize(), + op.Stop(), + ] + ) + res = check_safety(pickled) + self.assertGreater(res.severity, Severity.LIKELY_SAFE) + self.assertEqual( + res.detailed_results()["AnalysisResult"].get("UnsafeImports"), + "from multiprocessing.util import spawnv_passfds", + )
eb299b453342Check the root module name during unsafe import detection
1 file changed · +2 −2
fickling/fickle.py+2 −2 modified@@ -864,7 +864,7 @@ def is_likely_safe(self): def unsafe_imports(self) -> Iterator[ast.Import | ast.ImportFrom]: for node in self.properties.imports: - if node.module in ( + if node.module and node.module.split(".")[0] in ( "__builtin__", "__builtins__", "builtins", @@ -1157,7 +1157,7 @@ def run(self, interpreter: Interpreter): f"Module: {type(module).__name__}, Attr: {type(attr).__name__}" ) - if not module.isidentifier() or not attr.isidentifier(): + if not all(m.isidentifier() for m in module.split(".")) or not attr.isidentifier(): raise ValueError( f"Extracted identifiers are not valid Python identifiers. " f"Module: {module!r}, Attr: {attr!r}"
6b400e1a2525Add missing reference to GHSA-q5qq-mvfm-j35x in test_missing_runpy()
1 file changed · +1 −0
test/test_bypasses.py+1 −0 modified@@ -85,6 +85,7 @@ def test_missing_marshal_and_types(self): self.assertGreater(check_safety(opcodes).severity, Severity.LIKELY_SAFE) # https://github.com/trailofbits/fickling/security/advisories/GHSA-wfq2-52f7-7qvj + # https://github.com/trailofbits/fickling/security/advisories/GHSA-q5qq-mvfm-j35x def test_missing_runpy(self): pickled = Pickled( [
b793563e60a5Add pydoc and ctypes to unsafe imports
2 files changed · +84 −0
fickling/fickle.py+2 −0 modified@@ -880,6 +880,8 @@ def unsafe_imports(self) -> Iterator[ast.Import | ast.ImportFrom]: "types", "runpy", "cProfile", + "ctypes", + "pydoc", ): yield node elif "eval" in (n.name for n in node.names):
test/test_bypasses.py+82 −0 modified@@ -139,3 +139,85 @@ def test_missing_cprofile(self): res.detailed_results()["AnalysisResult"].get("UnsafeImports"), "from cProfile import run", ) + + # https://github.com/trailofbits/fickling/security/advisories/GHSA-q5qq-mvfm-j35x + # https://github.com/trailofbits/fickling/security/advisories/GHSA-5hvc-6wx8-mvv4 + def test_missing_ctypes(self): + pickled = Pickled( + [ + op.Proto.create(5), + op.ShortBinUnicode("builtins"), + op.Memoize(), + op.ShortBinUnicode("getattr"), + op.Memoize(), + op.StackGlobal(), + op.Memoize(), + op.ShortBinUnicode("ctypes"), + op.Memoize(), + op.ShortBinUnicode("CDLL"), + op.Memoize(), + op.StackGlobal(), + op.Memoize(), + op.ShortBinUnicode("libc.dylib"), + op.Memoize(), + op.TupleOne(), + op.Memoize(), + op.Reduce(), + op.Memoize(), + op.ShortBinUnicode("system"), + op.Memoize(), + op.TupleTwo(), + op.Memoize(), + op.Reduce(), + op.Memoize(), + op.ShortBinBytes(b"id"), + op.Memoize(), + op.TupleOne(), + op.Memoize(), + op.Reduce(), + op.Memoize(), + op.Stop(), + ] + ) + res = check_safety(pickled) + self.assertGreater(res.severity, Severity.LIKELY_SAFE) + self.assertEqual( + res.detailed_results()["AnalysisResult"].get("UnsafeImports"), + "from ctypes import CDLL", + ) + + # https://github.com/trailofbits/fickling/security/advisories/GHSA-5hvc-6wx8-mvv4 + def test_missing_pydoc(self): + pickled = Pickled( + [ + op.Global("pydoc locate"), + op.String("ctypes.windll.kernel32.WinExec"), + op.TupleOne(), + op.Reduce(), + op.Put(0), + op.Pop(), + op.Get(0), + op.ShortBinBytes(b"calc.exe"), + op.BinInt1(1), + op.TupleTwo(), + op.Reduce(), + op.Put(1), + op.Pop(), + op.Global("builtins Exception"), + op.EmptyTuple(), + op.Reduce(), + op.Put(2), + op.EmptyDict(), + op.String("rce_status"), + op.Get(1), + op.SetItem(), + op.Build(), + op.Stop(), + ] + ) + res = check_safety(pickled) + self.assertGreater(res.severity, Severity.LIKELY_SAFE) + self.assertEqual( + res.detailed_results()["AnalysisResult"].get("UnsafeImports"), + "from pydoc import locate", + )
9a2b3f89bd05Add runpy to unsafe modules
2 files changed · +29 −0
fickling/fickle.py+1 −0 modified@@ -878,6 +878,7 @@ def unsafe_imports(self) -> Iterator[ast.Import | ast.ImportFrom]: "pty", "marshal", "types", + "runpy", ): yield node elif "eval" in (n.name for n in node.names):
test/test_bypasses.py+28 −0 modified@@ -83,3 +83,31 @@ def test_missing_marshal_and_types(self): ) self.assertGreater(check_safety(opcodes).severity, Severity.LIKELY_SAFE) + + # https://github.com/trailofbits/fickling/security/advisories/GHSA-wfq2-52f7-7qvj + def test_missing_runpy(self): + pickled = Pickled( + [ + op.Proto.create(5), + op.Frame(46), + op.ShortBinUnicode("runpy"), + op.Memoize(), + op.ShortBinUnicode("run_path"), + op.Memoize(), + op.StackGlobal(), + op.Memoize(), + op.ShortBinUnicode("/tmp/malicious.py"), + op.Memoize(), + op.TupleOne(), + op.Memoize(), + op.Reduce(), + op.Memoize(), + op.Stop(), + ] + ) + res = check_safety(pickled) + self.assertGreater(res.severity, Severity.LIKELY_SAFE) + self.assertEqual( + res.detailed_results()["AnalysisResult"].get("UnsafeImports"), + "from runpy import run_path", + )
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
11- github.com/advisories/GHSA-q5qq-mvfm-j35xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-22609ghsaADVISORY
- github.com/trailofbits/fickling/blob/977b0769c13537cd96549c12bb537f05464cf09c/test/test_bypasses.pyghsaWEB
- github.com/trailofbits/fickling/commit/29d5545e74b07766892c1f0461b801afccee4f91ghsax_refsource_MISCWEB
- github.com/trailofbits/fickling/commit/6b400e1a2525e6a4a076c97ccc0d4d9581317101ghsaWEB
- github.com/trailofbits/fickling/commit/9a2b3f89bd0598b528d62c10a64c1986fcb09f66ghsax_refsource_MISCWEB
- github.com/trailofbits/fickling/commit/b793563e60a5e039c5837b09d7f4f6b92e6040d1ghsax_refsource_MISCWEB
- github.com/trailofbits/fickling/commit/eb299b453342f1931c787bcb3bc33f3a03a173f9ghsax_refsource_MISCWEB
- github.com/trailofbits/fickling/pull/195ghsaWEB
- github.com/trailofbits/fickling/releases/tag/v0.1.7ghsax_refsource_MISCWEB
- github.com/trailofbits/fickling/security/advisories/GHSA-q5qq-mvfm-j35xghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.