PraisonAI Dynamic Context history and terminal tools read files outside configured storage via path traversal
PraisonAI's Dynamic Context module provides filesystem-backed history and terminal-log storage. The SDK reference describes the module as providing:
The module also exports agent-callable tool factories:
create_history_tools() returns history_search, history_tail, and
history_get.create_terminal_tools() returns terminal_tail, terminal_grep, and
terminal_commands.Those tools accept run_id and agent_id arguments from the tool caller. The
underlying stores join those values into filesystem paths without rejecting
absolute paths or .. traversal:
history_dir = self.base_dir / run_id / "history"
return history_dir / f"{agent_id}.jsonl"
terminal_dir = self.base_dir / run_id / "terminal"
return terminal_dir / f"{agent_id}.log"
Because run_id can be an absolute path and agent_id can contain traversal,
a lower-trust prompt/user that can call these tools can read .jsonl and
.log files outside the configured Dynamic Context base directory.
MervinPraison/PraisonAIpippraisonaisrc/praisonai/praisonai/context/history_store.pysrc/praisonai/praisonai/context/terminal_logger.py4.6.58origin/main validated:
1ad58ca02975ff1398efeda694ea2ab78f20cf3eorigin/main tag validated: v4.6.58Suggested affected range:
pip:praisonai >= 3.8.1, <= 4.6.58
Representative local sweep:
3.8.1: vulnerable4.0.0: vulnerable4.5.113: vulnerable4.6.33: vulnerable4.6.34: vulnerable4.6.40: vulnerable4.6.50: vulnerable4.6.58: vulnerableHistoryStore._get_history_path() and TerminalLogger._get_log_path() treat
logical identifiers as path segments, but never validate that the resolved path
stays under base_dir.
History path construction:
def _get_history_path(self, run_id: str, agent_id: str) -> Path:
history_dir = self.base_dir / run_id / "history"
history_dir.mkdir(parents=True, exist_ok=True)
return history_dir / f"{agent_id}.jsonl"
Terminal path construction:
def _get_log_path(self, run_id: str, agent_id: str) -> Path:
terminal_dir = self.base_dir / run_id / "terminal"
terminal_dir.mkdir(parents=True, exist_ok=True)
return terminal_dir / f"{agent_id}.log"
The agent tools pass caller-controlled run_id and agent_id directly into
these helpers:
def history_tail(agent_id: str = "default", run_id: str = "default", count: int = 10) -> str:
messages = history_store.get_last_messages(agent_id=agent_id, run_id=run_id, count=count)
def terminal_tail(agent_id: str = "default", run_id: str = "default", lines: int = 50) -> str:
return term_logger.tail_session(agent_id=agent_id, run_id=run_id, lines=lines)
There is no check equivalent to:
resolved = candidate.resolve()
base = self.base_dir.resolve()
resolved.relative_to(base)
There is also no identifier allowlist preventing /, \, or .. in
run_id or agent_id.
Run against the latest PyPI package:
uv run --with 'praisonai==4.6.58' \
python poc/pov_prai_cand_027_history_terminal_tools_path_traversal.py --json
The PoV:
secret.jsonl and
secret.log.history_tail() and history_get() with
run_id=<outside-dir> and agent_id=../secret.terminal_tail() and terminal_grep() with the same traversal.Observed output summary from evidence/pov-pypi-4.6.58.json:
{
"package": "praisonai",
"package_version": "4.6.58",
"controls": {
"valid_history_read_works": true,
"valid_terminal_read_works": true,
"outside_history_file_outside_base_dir": true,
"outside_terminal_file_outside_base_dir": true,
"traversal_history_path_resolves_to_outside_file": true,
"traversal_terminal_path_resolves_to_outside_file": true
},
"outside_history_tail": "Last 1 messages:\\n\\n[system]: PRAI-CAND-027-HISTORY-SECRET",
"outside_terminal_tail": "PRAI-CAND-027-TERMINAL-SECRET\\nsecond line\\n",
"outside_terminal_grep": "Found 1 matches:\\n\\n--- Line 1 ---\\n> PRAI-CAND-027-TERMINAL-SECRET\\n second line",
"vulnerable": true
}
The PoV is local-only. It does not start a server, contact a third-party target, or use real credentials.
This report does not claim that history and terminal helpers should be unable
to read legitimate history or terminal logs. The issue is narrower: logical
run_id and agent_id values can escape the configured Dynamic Context base
directory.
The controls show the intended boundary:
.jsonl and .log files are not under the configured
base_dir; andThe official context reference describes history persistence and terminal logging as filesystem-backed Dynamic Context features. The context security documentation also treats absolute paths, path traversal, and sensitive files as privacy/security risks. Reading files outside the configured context store conflicts with that documented boundary.
If a PraisonAI application exposes these Dynamic Context tools to untrusted or lower-trust prompts, the lower-trust caller can read files outside the configured context storage when the target file can be reached with the tool-imposed suffix:
history_* tools can disclose reachable .jsonl files;terminal_* tools can disclose reachable .log files; andThis can expose conversation history, prompts, terminal output, command logs, tokens, API keys, cloud credentials, operational data, or other secrets stored in JSONL/log files readable by the PraisonAI process.
The impact is confidentiality-only in the tested surface. Integrity and availability are not claimed for this report.
Suggested severity: High.
Rationale:
AV: applies when an application exposes an agent with these tools over a
network chat/API surface.AC: the traversal needs only chosen run_id and agent_id values.PR: an unauthenticated or public-facing agent endpoint can be exploited
without an account. Deployments that require authenticated chat/API access
may score this as PR:L.UI: the attacker directly supplies the prompt/tool argument to the
exposed agent surface.C: conversation history and terminal logs can contain secrets and private
operational data.I:N/A: this report demonstrates read-only disclosure.Treat run_id and agent_id as logical identifiers, not path components.
Recommended fixes:
run_id and agent_id..resolve(), and reject any path that is not
under self.base_dir.resolve().run_id, ../ in run_id, and ../ in
agent_id for history and terminal tool factories.Minimal containment shape:
def _safe_child(self, *parts: str) -> Path:
candidate = self.base_dir.joinpath(*parts).resolve()
base = self.base_dir.resolve()
try:
candidate.relative_to(base)
except ValueError as exc:
raise PermissionError("Context path is outside configured base_dir") from exc
return candidate
Pair this with an identifier allowlist, because run_id and agent_id should
not need filesystem syntax.
왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도 | 75.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 75.00 |
VPI 공식 vpi-v1 기준