GitHub Advisoryadvisory · PyPI
GHSA-ch89-h4r2-c8f8 PraisonAI: [Path Traversal] agent tools escape the configured workspace via symlinks
HIGH
Official page ↗
- Aliases
- CVE-2026-55540
- Ecosystem
- PyPI
- Packages
- PraisonAI
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:L
- Weaknesses
- CWE-22
- Reviewed
- 2026-08-25
Affected packages
• PyPI PraisonAI: < 4.6.58 → fixed in 4.6.58
Description
Summary
PraisonAI's `praisonai.code` tool wrappers (exported as `CODE_TOOLS` for agents) expose a `workspace` setting that the module itself treats as a path-traversal security boundary — `read_file`, `write_file`, `apply_diff`, and `search_replace` explicitly call `is_path_within_directory()` and return `"… is outside the workspace"` on violations. That boundary is enforced unsoundly and inconsistently:
1. The containment helper uses `os.path.abspath()`, not `realpath()`/`Path.resolve()`. A symlink located inside the workspace whose target is outside has an `abspath()` that is still inside the workspace, so it passes the check while `open()` follows the link. This bypasses read, write, apply_diff, and search_replace (CWE-59).
2. `list_files()` resolves `path` against the workspace but never calls the containment helper at all — `../` and absolute paths escape directly (CWE-22).
3. `execute_command()` takes a `workspace` argument documented "for security validation" but performs no `cwd` containment check; `code_execute_command()` resolves a relative `cwd` against the workspace and also never validates it (and never even passes `workspace` to the low-level helper). A relative `cwd="../outside"` runs commands from outside the workspace (CWE-22).
An attacker who can influence an agent that has these tools attached (untrusted prompt, indirect prompt injection, or a server-exposed agent) can read, overwrite, list, and execute from outside the configured workspace, bounded only by the process user's filesystem permissions.
Technical Detail
1. Unsound containment helper (symlink bypass — CWE-59)
```python
src/praisonai/praisonai/code/utils/file_utils.py — is_path_within_directory()
abs_file = os.path.abspath(file_path) # does NOT resolve symlinks
abs_dir = os.path.abspath(directory)
if not abs_dir.endswith(os.sep): abs_dir += os.sep
return abs_file.startswith(abs_dir) or abs_file == abs_dir.rstrip(os.sep)
```
`read_file`/`write_file`/`apply_diff`/`search_replace` call this with the configured workspace (e.g. `read_file.py`: `# Security check - ensure path is within workspace`). Because `abspath()` does not canonicalize symlinks, a link at `WORKSPACE/link_to_secret.txt` → `/outside/secret.txt` has `abspath` `WORKSPACE/link_to_secret.txt` (inside) and passes, while `open()` follows it to the real outside target.
2. `list_files()` has no containment check (CWE-22)
```python
src/praisonai/praisonai/code/tools/list_files.py
if workspace and not os.path.isabs(path):
abs_path = os.path.abspath(os.path.join(workspace, path)) # ../ collapses out of workspace
else:
abs_path = os.path.abspath(path) # absolute path used as-is
... os.path.isdir(abs_path) then listed. is_path_within_directory() is NEVER called.
```
3. `execute_command()` never validates `cwd` (CWE-22)
```python
src/praisonai/praisonai/code/tools/execute_command.py — workspace param doc: "for security validation"
if cwd:
if workspace and not os.path.isabs(cwd):
work_dir = os.path.abspath(os.path.join(workspace, cwd)) # ../ escapes; no containment check
else:
work_dir = os.path.abspath(cwd)
subprocess.run(args, cwd=work_dir, ...) # no is_path_within_directory() anywhere
```
```python
src/praisonai/praisonai/code/agent_tools.py — code_execute_command()
if work_dir and _workspace_root and not os.path.isabs(work_dir):
work_dir = os.path.join(_workspace_root, work_dir) # joins, never validates
result = _execute_command(command=command, cwd=work_dir, timeout=120) # workspace not even passed
```
Note: `execute_command` rejects `shell=True` and runs `shlex.split(command)` via `subprocess.run` (no shell), so shell metacharacters (`&&`, `>`, pipes) do not work — but any binary still runs with attacker-chosen argv from the escaped cwd, which is sufficient to read/write outside the workspace.
The workspace is an intended boundary (pre-empts "by design")
The module asserts this control itself: `read_file.py` "Security check - ensure path is within workspace"; `write_file.py` "default workspace is cwd so relative paths cannot escape"; `is_path_within_directory` docstring "(prevents path traversal)"; `execute_command` `workspace` param "for security validation". The bug is that the asserted control is unsound (abspath vs realpath) and not applied to `list_files`/`execute_command` cwd.
Proof of Concept
Self-contained, local temp fixtures only; no network, no untrusted commands. Real `praisonai.code` agent tools were called.
```
workspace = /tmp/.../workspace outside = /tmp/.../outside
[1] baseline plain ../ read -> BLOCKED: "Path '../outside/secret.txt' is outside the workspace"
[2] symlink read (in-WS link) -> SUCCESS: returned "SECRET_OUTSIDE_WORKSPACE"
[3] symlink write (in-WS link) -> SUCCESS: outside file now contains "OVERWRITTEN_VIA_SYMLINK"
[4] code_list_files("../outside") -> SUCCESS: "Contents of ../outside: 📄 secret.txt"
[5] code_execute_command(cwd="../outside","pwd") -> SUCCESS: stdout "/tmp/.../outside"
[6] code_execute_command(cwd="../outside", python3 -c open('planted.txt','w')...)
-> SUCCESS: new file created OUTSIDE workspace, "PWNED_OUTSIDE_WORKSPACE"
```
Steps 2–6 each cross the configured workspace boundary; step 1 shows the plain-`../` guard that the symlink and unscoped vectors bypass.
Impact
• Confidentiality: read files outside the workspace (in-workspace symlink; or list/enumerate outside dirs via `list_files`).
• Integrity: overwrite outside files via symlink; create/modify files outside the workspace via `execute_command` running in an escaped cwd.
• Execution boundary: run arbitrary available binaries (argv-controlled) from a directory outside the workspace.
Bounded by the process user's permissions. In a code-agent or server-exposed agent processing untrusted input, this exposes secrets / project-adjacent / host files and breaks the project-boundary integrity guarantee the workspace setting advertises.
Suggested Fix
• Replace `is_path_within_directory()` with a `realpath()` / `Path.resolve()`-based containment check, and compare with `os.path.commonpath()` rather than `startswith`.
• Apply that check consistently to every file path, directory path, backup path, diff/search-replace target, and command working directory, after full canonicalization (resolve the symlink's real target, not the link path).
• `list_files()`: reject absolute paths and `../` escapes when `workspace` is set.
• `execute_command()`: validate `cwd` containment when `workspace` is set; `code_execute_command()` should pass `_workspace_root` to the low-level helper or validate itself.
• Regression tests: symlink read/write/diff/search-replace to outside targets; `list_files("../outside", workspace=…)`; `execute_command(cwd="../outside", workspace=…)`; absolute outside paths with a workspace set.
Related
Alias · CVE (1)
- CVE-2026-55540 PraisonAI is a multi-agent teams system. vulnerability
Weakness · CWE (1)
- CWE-22 Improper Limitation of a Pathname to a Restricted Directory weakness