{"id":"CVE-2026-41496","aliases":["GHSA-rg3h-x3jw-7jm5","PYSEC-2026-2923","PYSEC-2026-2952"],"title":"PraisonAI: SQL Injection via unvalidated `table_prefix` in 9 conversation store backends (incomplete fix for CVE-2026-40315)","summary":"PraisonAI: SQL Injection via unvalidated `table_prefix` in 9 conversation store backends (incomplete fix for CVE-2026-40315)","severity":"high","cvss":8.1,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N","vendor":"praisonai","product":"praisonai","ecosystem":"pip","affected":["praisonai < 4.5.149","praisonaiagents < 1.6.8"],"patched":["praisonai 4.5.149","praisonaiagents 1.6.8"],"published":"2026-04-17","updated":"2026-07-13","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-rg3h-x3jw-7jm5","references":[{"url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-rg3h-x3jw-7jm5"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41496"},{"url":"https://github.com/MervinPraison/PraisonAI"}],"tags":["osv","pip"],"epss":0.00347,"epssPercentile":0.28384,"ingestedAt":"2026-07-13T18:58:02.764Z","slug":"CVE-2026-41496","body":"## Overview\n\nThe 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}`).\n\n## Affected packages\n\n- `praisonai < 4.5.149`\n- `praisonaiagents < 1.6.8`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `praisonai 4.5.149`\n- `praisonaiagents 1.6.8`","depth":"twilight","depthScore":45,"depthScoreParts":{"impact":44.6,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}