logo

Database

SQL injection In praisonaiagents

Description

PraisonAI: SQL Injection via unvalidated table_prefix in 9 conversation store backends (incomplete fix for CVE-2026-40315) The fix for CVE-2026-40315 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.

postgres.py additionally accepts an unvalidated schema parameter used directly in DDL.

Severity

High — CWE-89 (SQL Injection)

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N — 8.1

Exploitable 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.

Details

The CVE-2026-40315 fix added this guard to sqlite.py:52:

# sqlite.py — PATCHED
import re
if not re.match(r'^[a-zA-Z0-9_]*$', table_prefix):
    raise ValueError("table_prefix must contain only alphanumeric characters and underscores")

The following backends perform the identical table_prefix → f-string SQL pattern without this guard:

Backend
File
Line
Injection points

Additionally, praisonai-agents/praisonaiagents/storage/backends.py:179 (SQLiteBackend) accepts table_name without validation.

PoC

#!/usr/bin/env python3
"""
Demonstrates: sqlite.py rejects malicious table_prefix, mysql.py accepts it.
Run: python3 poc.py  (no dependencies required)
"""
import re

payload = "x'; DROP TABLE users; --"...

Output:

[SQLite] OK — rejected malicious table_prefix
[MySQL]  VULN — generated SQL:
  CREATE TABLE IF NOT EXISTS x'; DROP TABLE users; --sessions (session_id VARCHAR(255) PRIMARY KEY)
[Postgres] VULN — schema injection:
  CREATE SCHEMA IF NOT EXISTS public; DROP SCHEMA data CASCADE; --

Vulnerable code (mysql.py, representative)

# mysql.py:65-67 — NO validation
self.table_prefix = table_prefix                    # ← raw input
self.sessions_table = f"{table_prefix}sessions"     # ← into identifier
self.messages_table = f"{table_prefix}messages"

# mysql.py:105 — straight into DDL
cur.execute(f"""
    CREATE TABLE IF NOT EXISTS {self.sessions_table} (...

Compare with the patched sqlite.py:52:

# sqlite.py:52-53 — HAS validation
if not re.match(r'^[a-zA-Z0-9_]*$', table_prefix):
    raise ValueError("table_prefix must contain only alphanumeric characters and underscores")

Impact

When 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.

PostgreSQL's schema parameter adds a second injection vector in DDL (CREATE SCHEMA IF NOT EXISTS {schema}).

Mitigation

Update Impact

Minimal update. May introduce new vulnerabilities or breaking changes.

Ecosystem
Component
Affected version
Patched versions