PraisonAI: ZipSlip path traversal via unchecked tar symlink linkname in _safe_extractall
What's the vulnerability?
PraisonAI's _safe_extractall helper — used by all recipe
pull / publish / unpack flows — validates each tar member's name for
absolute paths, .. components, and resolved-path escape. However, it never
validates member.linkname.
An attacker can therefore include a symlink member whose link target points
outside dest_dir. That symlink passes the name-only check and is
extracted unchecked. A subsequent write entry in the same archive, traversing
through the just-created symlink, then writes its contents outside the
destination directory — a classic ZipSlip path traversal.
Any code path that pulls, publishes, or unpacks an attacker-supplied recipe archive is exploitable, since the recipe tar is the untrusted input.
Root Cause Analysis
# 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.
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00149 pruva-verify GHSA-9q28-ghcr-c4x3 pruva-verify CVE-2026-44340 curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00149/artifacts/reproduction_steps.sh Make executable
chmod +x reproduction_steps.sh Run the script
./reproduction_steps.sh How Pruva Reproduced This
Watch the AI agent's step-by-step process.
Loading session...
Artifacts
No artifacts available