## Ticket: CVE-2026-44340 — PraisonAI ZipSlip via unchecked tar symlink member

**Advisory**: GHSA-9q28-ghcr-c4x3 — https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-9q28-ghcr-c4x3
**CVE**: CVE-2026-44340 | **CWE-22** (Path Traversal — archive extraction / ZipSlip)
**Severity**: High — CVSS 8.7
**Package**: `praisonai` (PyPI) | **Repository**: https://github.com/MervinPraison/PraisonAI

### Impact

`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.

### Affected / fixed versions

Affected: `praisonai` `< 4.6.37`.
Fixed: **`4.6.37`**.

Reproduce on a vulnerable build (**`4.6.36`**) and verify the fix on
**`4.6.37`**.

### Where to look

The fix ships in the `4.6.37` release. Inspect the patch to confirm the root
cause and the guard:

```bash
git clone https://github.com/MervinPraison/PraisonAI.git
cd PraisonAI && git diff v4.6.36 v4.6.37
```

The change extends `_safe_extractall` to also validate `member.linkname`, so a
symlink member pointing outside `dest_dir` is rejected.

### Reproduction approach

No service, database, or browser is needed — the bug is observable purely on
the local filesystem.

1. In a fresh virtualenv, install the **vulnerable** build:
   `pip install praisonai==4.6.36`
2. Craft a tar archive with two members:
   - a symlink member whose `linkname` points outside `dest_dir`, and
   - a regular file member written *through* that symlink.
3. Feed the archive to the recipe-unpack code path that calls
   `_safe_extractall`, then check whether a file appeared outside `dest_dir`.
4. Repeat with the **fixed** build: `pip install praisonai==4.6.37`.

### Expected result

| Build | crafted tar (symlink-out + write-through) fed to `_safe_extractall` | Observable |
|-------|---------------------------------------------------------------------|-----------|
| `praisonai==4.6.36` (vulnerable) | `linkname` not validated; symlink extracted, write follows it | a file is **written outside `dest_dir`** |
| `praisonai==4.6.37` (fixed) | `linkname` validated; malicious symlink member rejected | **no file** is written outside `dest_dir` |

- **Vulnerable indicator**: after extraction, a file exists outside `dest_dir`
  (at the location the symlink target pointed to).
- **Fixed indicator**: `_safe_extractall` rejects the malicious member; nothing
  is written outside `dest_dir`.

### Expected artifacts

- `reproduction_steps.sh` — installs both versions, builds the crafted tar,
  runs the unpack PoC, and reports whether a file escaped `dest_dir` for each.
- `validation_verdict.json` — `verdict: confirmed`, with the vulnerable and
  fixed indicators captured.
- Logs capturing the script output for `4.6.36` and `4.6.37`.
