# REPRO-2026-00149: PraisonAI: ZipSlip path traversal via unchecked tar symlink linkname in _safe_extractall ## Summary Status: published Severity: high Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00149 GHSA: GHSA-9q28-ghcr-c4x3 CVE: CVE-2026-44340 ## Package Name: praisonai Ecosystem: pip Affected: < 4.6.37 (specifically < 4.6.36; earliest affected at least 2.7.2) Fixed: 4.6.37 ## Root Cause # RCA Report: CVE-2026-44340 — PraisonAI ZipSlip via unchecked tar symlink member ## Summary PraisonAI's `_safe_extractall` helper function in `src/praisonai/praisonai/recipe/registry.py` validates tar archive member names to prevent path traversal, but it does not validate `member.linkname` for symlink or hardlink members. This allows a malicious `.praison` recipe bundle containing a symlink with a relative `linkname` that escapes the destination directory (e.g., `../../outside`) to be extracted unchecked. The extracted symlink points outside the intended extraction directory, enabling a ZipSlip-style path traversal attack. ## Impact - **Package**: `praisonai` (PyPI) - **Repository**: https://github.com/MervinPraison/PraisonAI - **Affected versions**: `< 4.6.37` - **Fixed version**: `4.6.37` - **CWE**: CWE-22 (Path Traversal) - **Severity**: High — CVSS 8.7 - **Consequences**: Any code path that downloads, publishes, or unpacks an attacker-supplied recipe archive (via `LocalRegistry.pull()`, `HttpRegistry.pull()`, etc.) can be exploited to create symlinks outside the intended destination directory, potentially leading to arbitrary file write or overwrite on subsequent operations. ## Root Cause The `_safe_extractall` function iterates over all tar members and checks: 1. `member.name` is not absolute 2. `member.name` does not contain `..` components 3. The resolved path of `member.name` stays within `dest_dir` However, it never inspects `member.linkname`. When a tar member is a symlink (`tarfile.SYMTYPE`) or hardlink (`tarfile.LNKTYPE`), `tarfile.TarFile.extractall()` creates the link on disk using `member.linkname` as the target. If that target points outside `dest_dir`, the link escapes the sandbox. **Fix commit**: `0cec9fd` — "refactor: harden archive extraction and tool resolution boundary" - https://github.com/MervinPraison/PraisonAI/commit/0cec9fd The fix adds validation for symlink and hardlink members: - Rejects absolute `linkname` targets - Resolves the relative `linkname` against `dest_dir` and rejects targets that escape it - Also switches to `tar.extractall(dest_dir, filter="data")` on Python 3.12+ for additional hardening ## Reproduction Steps The reproduction script is `repro/reproduction_steps.sh`. It performs the following: 1. Creates two isolated virtualenvs and installs `praisonai==4.6.36` (vulnerable) and `praisonai==4.6.37` (fixed). 2. Builds a malicious `.praison` bundle (a gzipped tar) containing: - A valid `manifest.json` - A symlink member named `escape` with `linkname = ../../outside` 3. Sets up a minimal `LocalRegistry` filesystem structure pointing at the malicious bundle. 4. Invokes the user-facing `LocalRegistry.pull("testrecipe", "1.0.0", output_dir=..., verify_checksum=False)` API on both versions. 5. Captures whether the symlink was extracted and whether it resolves outside the output directory. **Expected evidence**: - **Vulnerable (4.6.36)**: `pull()` succeeds without error. A symlink `escape` is created inside the output directory, resolving to a path outside it (`../../outside`). - **Fixed (4.6.37)**: `pull()` raises `RegistryError: Refusing to extract link escaping target directory: escape -> ../../outside`. No symlink is extracted. ## Evidence - **Vulnerable run log**: `logs/vulnerable.log` - `RESULT: no_exception` - `PATH: .../repro/output/vuln/testrecipe` - Filesystem: symlink `escape -> ../../outside` exists and resolves outside the destination directory. - **Fixed run log**: `logs/fixed.log` - `RESULT: registry_error` - `ERROR: Refusing to extract link escaping target directory: escape -> ../../outside` - Filesystem: no symlink created. - **Runtime manifest**: `repro/runtime_manifest.json` - Contains `verdict: confirmed`, structured pass/fail booleans, and captured logs for both versions. ## Recommendations / Next Steps 1. **Upgrade immediately** to `praisonai >= 4.6.37`. 2. **Validate archive extraction logic** across the entire codebase. Any custom `extractall` wrapper should validate both member names and link targets. 3. **Add regression tests** for symlink and hardlink traversal, as the upstream project did in `test_safe_extractall_symlink.py`. 4. **Use `tarfile.extractall(filter="data")`** on Python 3.12+ to benefit from built-in data-only extraction filters. 5. **Consider disabling symlink/hardlink extraction entirely** in recipe bundles if they are not required for functionality. ## Additional Notes - **Idempotency**: `repro/reproduction_steps.sh` was run twice consecutively and produced identical results both times. - **Limitations**: The reproduction demonstrates symlink extraction outside `dest_dir`. The actual write-through follow-up attack (a regular file member whose path traverses the created symlink) depends on `tarfile.extractall()` behavior and Python version; the core vulnerability is the unchecked symlink extraction itself, which is what the upstream fix addresses. - **Environment**: Python 3.11, Ubuntu-based container. ## Reproduction Details Reproduced: 2026-05-22T18:09:17.939Z Duration: 1062 seconds Tool calls: 158 Turns: 134 Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00149 pruva-verify GHSA-9q28-ghcr-c4x3 pruva-verify CVE-2026-44340 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00149&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00149/artifacts/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/advisories/GHSA-9q28-ghcr-c4x3 - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-44340 - Source: MervinPraison/PraisonAI ## Artifacts - repro/rca_report.md (analysis, 5020 bytes) - repro/reproduction_steps.sh (reproduction_script, 6568 bytes) - vuln_variant/rca_report.md (analysis, 6558 bytes) - vuln_variant/reproduction_steps.sh (reproduction_script, 2240 bytes) - bundle/context.json (other, 2892 bytes) - bundle/metadata.json (other, 704 bytes) - bundle/ticket.md (ticket, 3399 bytes) - repro/.venv_fix/pyvenv.cfg (other, 216 bytes) - repro/.venv_fix/bin/distro (other, 272 bytes) - repro/.venv_fix/bin/idna (other, 267 bytes) - repro/.venv_fix/bin/markdown-it (other, 280 bytes) - repro/.venv_fix/bin/tqdm (other, 267 bytes) - repro/.venv_fix/bin/pip3 (other, 281 bytes) - repro/.venv_fix/bin/tiny-agents (other, 291 bytes) - repro/.venv_fix/bin/activate.fish (other, 2245 bytes) - repro/.venv_fix/bin/pip3.11 (other, 281 bytes) - repro/.venv_fix/bin/hf (other, 281 bytes) - repro/.venv_fix/bin/litellm (other, 278 bytes) - repro/.venv_fix/bin/normalizer (other, 293 bytes) - repro/.venv_fix/bin/huggingface-cli (other, 293 bytes) - repro/.venv_fix/bin/praisonai (other, 277 bytes) - repro/.venv_fix/bin/markdown_py (other, 274 bytes) - repro/.venv_fix/bin/httpx (other, 264 bytes) - repro/.venv_fix/bin/activate.csh (other, 969 bytes) - repro/.venv_fix/bin/Activate.ps1 (other, 9033 bytes) - repro/.venv_fix/bin/pip (other, 281 bytes) - repro/.venv_fix/bin/dotenv (other, 272 bytes) - repro/.venv_fix/bin/uvicorn (other, 271 bytes) - repro/.venv_fix/bin/setup-conda-env (other, 290 bytes) - repro/.venv_fix/bin/activate (other, 1745 bytes) - repro/.venv_fix/bin/pygmentize (other, 275 bytes) - repro/.venv_fix/bin/mcp (other, 264 bytes) - repro/.venv_fix/bin/praisonai-call (other, 277 bytes) - repro/.venv_fix/bin/litellm-proxy (other, 281 bytes) - repro/.venv_fix/bin/typer (other, 268 bytes) - repro/.venv_fix/bin/jsonschema (other, 273 bytes) - repro/output/vuln/testrecipe/manifest.json (other, 78 bytes) - repro/runtime_manifest.json (other, 502 bytes) - repro/malicious.tar.gz (other, 101 bytes) - repro/validation_verdict.json (other, 1408 bytes) - repro/registry/recipes/testrecipe/1.0.0/testrecipe-1.0.0.praison (other, 193 bytes) - repro/registry/index.json (other, 304 bytes) - repro/write_manifest.py (script, 788 bytes) - repro/test_zipslip.py (script, 990 bytes) - repro/.venv_vuln/pyvenv.cfg (other, 217 bytes) - repro/.venv_vuln/bin/distro (other, 273 bytes) - repro/.venv_vuln/bin/idna (other, 268 bytes) - repro/.venv_vuln/bin/markdown-it (other, 281 bytes) - repro/.venv_vuln/bin/tqdm (other, 268 bytes) - repro/.venv_vuln/bin/pip3 (other, 282 bytes) - repro/.venv_vuln/bin/tiny-agents (other, 292 bytes) - repro/.venv_vuln/bin/activate.fish (other, 2248 bytes) - repro/.venv_vuln/bin/pip3.11 (other, 282 bytes) - repro/.venv_vuln/bin/hf (other, 282 bytes) - repro/.venv_vuln/bin/litellm (other, 279 bytes) - repro/.venv_vuln/bin/normalizer (other, 294 bytes) - repro/.venv_vuln/bin/huggingface-cli (other, 294 bytes) - repro/.venv_vuln/bin/praisonai (other, 278 bytes) - repro/.venv_vuln/bin/markdown_py (other, 275 bytes) - repro/.venv_vuln/bin/httpx (other, 265 bytes) - repro/.venv_vuln/bin/activate.csh (other, 972 bytes) - repro/.venv_vuln/bin/Activate.ps1 (other, 9033 bytes) - repro/.venv_vuln/bin/pip (other, 282 bytes) - repro/.venv_vuln/bin/dotenv (other, 273 bytes) - repro/.venv_vuln/bin/uvicorn (other, 272 bytes) - repro/.venv_vuln/bin/setup-conda-env (other, 291 bytes) - repro/.venv_vuln/bin/activate (other, 1748 bytes) - repro/.venv_vuln/bin/pygmentize (other, 276 bytes) - repro/.venv_vuln/bin/mcp (other, 265 bytes) - repro/.venv_vuln/bin/praisonai-call (other, 278 bytes) - repro/.venv_vuln/bin/litellm-proxy (other, 282 bytes) - repro/.venv_vuln/bin/typer (other, 269 bytes) - repro/.venv_vuln/bin/jsonschema (other, 274 bytes) - vuln_variant/.venv_latest/pyvenv.cfg (other, 226 bytes) - vuln_variant/.venv_latest/bin/distro (other, 282 bytes) - vuln_variant/.venv_latest/bin/idna (other, 277 bytes) - vuln_variant/.venv_latest/bin/markdown-it (other, 290 bytes) - vuln_variant/.venv_latest/bin/tqdm (other, 277 bytes) - vuln_variant/.venv_latest/bin/pip3 (other, 291 bytes) - vuln_variant/.venv_latest/bin/tiny-agents (other, 301 bytes) - vuln_variant/.venv_latest/bin/activate.fish (other, 2261 bytes) - vuln_variant/.venv_latest/bin/pip3.11 (other, 291 bytes) - vuln_variant/.venv_latest/bin/hf (other, 291 bytes) - vuln_variant/.venv_latest/bin/litellm (other, 288 bytes) - vuln_variant/.venv_latest/bin/normalizer (other, 303 bytes) - vuln_variant/.venv_latest/bin/huggingface-cli (other, 303 bytes) - vuln_variant/.venv_latest/bin/praisonai (other, 287 bytes) - vuln_variant/.venv_latest/bin/markdown_py (other, 284 bytes) - vuln_variant/.venv_latest/bin/httpx (other, 274 bytes) - vuln_variant/.venv_latest/bin/activate.csh (other, 985 bytes) - vuln_variant/.venv_latest/bin/Activate.ps1 (other, 9033 bytes) - vuln_variant/.venv_latest/bin/pip (other, 291 bytes) - vuln_variant/.venv_latest/bin/dotenv (other, 282 bytes) - vuln_variant/.venv_latest/bin/uvicorn (other, 281 bytes) - vuln_variant/.venv_latest/bin/setup-conda-env (other, 300 bytes) - vuln_variant/.venv_latest/bin/activate (other, 1761 bytes) - vuln_variant/.venv_latest/bin/pygmentize (other, 285 bytes) - vuln_variant/.venv_latest/bin/mcp (other, 274 bytes) - vuln_variant/.venv_latest/bin/praisonai-call (other, 287 bytes) - vuln_variant/.venv_latest/bin/litellm-proxy (other, 291 bytes) - vuln_variant/.venv_latest/bin/typer (other, 278 bytes) - vuln_variant/.venv_latest/bin/jsonschema (other, 283 bytes) - vuln_variant/test_variants.py (script, 6804 bytes) - vuln_variant/patch_analysis.md (documentation, 5168 bytes) - vuln_variant/variant_manifest.json (other, 2432 bytes) - vuln_variant/validation_verdict.json (other, 1373 bytes) - vuln_variant/source_identity.json (other, 755 bytes) - logs/variant_attempts.log (log, 2329 bytes) - logs/vulnerable.log (log, 105 bytes) - logs/fixed.log (log, 106 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00149 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00149/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00149 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev