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.
One Command

Verify with pruva-verify

Run the Pruva CLI to automatically fetch and execute the reproduction script.

pruva-verify REPRO-2026-00149
or pruva-verify GHSA-9q28-ghcr-c4x3
or pruva-verify CVE-2026-44340
Install: curl -fsSL https://pruva.dev/install.sh | sh

Or Run Manually

1

Download the script

curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00149/artifacts/reproduction_steps.sh
2

Make executable

chmod +x reproduction_steps.sh
3

Run the script

./reproduction_steps.sh
Run in a VM, container, or disposable environment. This exploits a real vulnerability.

How Pruva Reproduced This

Watch the AI agent's step-by-step process.

Loading session...

Artifacts

No artifacts available