More writeups publishing as disclosures close. Mid-2026.
PraisonAI accepts MCP server configurations via the --mcp CLI argument. This argument is passed directly to shlex.split() and forwarded through the call chain into anyio.open_process(), a subprocess execution sink, with no validation at any hop.
# unsanitized input split, no allowlist check parts = shlex.split(command) # raw result forwarded downstream unchanged cmd, args, env = self.parse_mcp_command(command, env_vars) # attacker-controlled cmd packed into server params self.server_params = StdioServerParameters(command=cmd, args=arguments) # sink: executed as subprocess with no sanitization process = await anyio.open_process([command, *args])
The developer assumed the --mcp argument would always be operator-supplied and therefore trusted. There is no allowlist of permitted commands, no subprocess isolation, and no validation that the supplied string is a legitimate MCP server invocation.
Passing user-controlled strings to shlex.split() followed by a subprocess call is a well-understood sink pattern. The failure here is architectural: trust was assumed at the boundary rather than enforced.
An attacker who can influence the --mcp argument achieves full OS command execution as the process user. No authentication required. In any deployment where PraisonAI is exposed to untrusted input, networked environments, or multi-tenant setups, this is directly exploitable.
The maintainer addressed this in v4.5.69 by introducing a command allowlist. Only pre-approved executables are now permitted.
ALLOWED_COMMANDS = {"npx", "uvx", "node", "python"}
if cmd not in ALLOWED_COMMANDS:
raise ValueError(f"Disallowed command: {cmd}")| Date | Event |
|---|---|
| Feb 27, 2026 | Vulnerability identified and reported to maintainer |
| Feb 27, 2026 | CVE ID requested from MITRE (MCID15649049) |
| Mar 16, 2026 | Maintainer shipped fix in v4.5.69 (commit 47bff65) |
| Mar 24, 2026 | Public disclosure |
AGiXT exposes file operation commands through its extension system: write_to_file, read_file, modify_file, and delete_file. Each calls safe_join() to construct the target path. Despite the name, safe_join() uses os.path.normpath() to resolve the path but never checks that the result stays within the workspace boundary.
# attacker-controlled filename enters via POST request command_args = command.command_args # forwarded unchanged into command execution response = await Extensions(...).execute_command(command_name=command_name, command_args=command_args) # sink: path resolved but never boundary-checked new_path = os.path.normpath(os.path.join(self.WORKING_DIRECTORY, *paths.split("/"))) # new_path is never verified to start with WORKING_DIRECTORY
The function is named safe_join(), which implies boundary enforcement, but the implementation only calls os.path.normpath(). This resolves ../ sequences syntactically but does not verify the result stays within the intended directory. The correct pattern already existed in Memory.py:195-201 within the same codebase.
The developer applied the correct pattern in one location but not the other. A common failure mode in fast-moving codebases: security patterns applied in one place, not propagated to similar locations added later.
base = os.path.realpath(self.WORKING_DIRECTORY)
new_path = os.path.realpath(
os.path.normpath(os.path.join(self.WORKING_DIRECTORY, *paths.split("/")))
)
if not new_path.startswith(base + os.sep) and new_path != base:
raise PermissionError(f"Path traversal detected: {paths!r}")| Date | Event |
|---|---|
| Mar 14, 2026 | Vulnerability identified and reported to AGiXT team |
| Mar 16, 2026 | Maintainer pushed fix in commit 2079ea5 |
| Mar 16, 2026 | CVE ID requested from MITRE |
| Jun 12, 2026 | Public disclosure |
AGiXT exposes a Custom API Endpoint command that accepts a URL from the user. That URL is passed directly to requests.request() in custom_api() with no allowlist, no blocklist, and no validation against internal address ranges.
# attacker-controlled URL enters via POST request command_args = command.command_args # sink: URL passed directly to requests with no validation response = requests.request( method=method, url=url, headers=headers, json=data )
The developer treated the URL parameter as a trusted operator-supplied value. There is no blocklist for loopback addresses, no IMDS protection, and no redirect-following restrictions. The requests library follows redirects by default, enabling redirect-based SSRF chains where an attacker bounces through a public URL to reach an internal target.
On any cloud-deployed AGiXT instance, an attacker can reach the Instance Metadata Service with a single request:
Target: http://169.254.169.254/latest/meta-data/iam/security-credentials/
Result: AWS/GCP/Azure temporary IAM credentials returned to attackerdef is_safe_url(url: str) -> bool:
blocked_ranges = [
"127.0.0.0/8", "169.254.0.0/16",
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"
]
...
if not is_safe_url(url):
raise ValueError("URL targets a restricted address range")| Date | Event |
|---|---|
| Mar 10, 2026 | Vulnerability identified and reported to AGiXT team |
| Mar 12, 2026 | Maintainer pushed fix in commit 711f507 |
| Mar 12, 2026 | CVE ID requested from MITRE |
| Jun 8, 2026 | Public disclosure |