CVE-2026-33980

CVSS 8.3 · Élevée
Type
Recherche de vulnérabilités
Publié le
Durée de lecture
8 min de lecture
Auteur
rootzero
Read in English
Read in English →

CVE-2026-33980 : injection KQL dans Azure Data Explorer MCP Server via table_name

Trois outils de métadonnées d’adx-mcp-server inséraient table_name dans du KQL : un agent pouvait lancer n’importe quelle requête sans passer par execute_query.

Pour décider

Impact
Un outil présenté comme une lecture de métadonnées peut construire une requête KQL arbitraire.
Conditions
Un client doit pouvoir appeler les outils MCP concernés. Les effets restent liés aux droits du compte ADX.
Action recommandée
Vérifier la présence du correctif 0abe0ee dans le code déployé. L’analyse ne confirme pas de version PyPI corrigée.

Fiche de l’avis

Paquet
pypi · adx-mcp-server
Versions affectées
≤ 1.1.0
Corrigé dans
Voir l’avis
CVSS
8.3 Élevée
Faiblesse
CWE-943
Accès requis
Compte requis
Avis publié
Crédit
rootzero

Synthèse de l’enquête

Les outils get_table_schema, sample_table_data et get_table_details d’adx-mcp-server construisaient leurs requêtes Kusto en insérant l’argument table_name dans des f-strings. Tout client MCP, y compris un agent manipulé par injection de prompt, pouvait transformer un appel à ces outils de métadonnées en requête KQL arbitraire ou en commande de gestion, hors de la politique d’approbation appliquée à execute_query. Corrigé dans le commit 0abe0ee, qu’aucune version publiée sur PyPI ne contient encore.

Quand un outil de schéma accepte du KQL

Nous examinions des serveurs MCP qui placent des outils de lecture « simples » devant des plateformes de données cloud. Dans ADX MCP Server, execute_query annonce clairement qu’il exécute du KQL libre, alors que les outils de schéma et d’échantillonnage semblent plus bornés. Nous avons suivi leur argument table_name jusqu’à client.execute() et trouvé trois requêtes construites par f-string. Un nom de table pouvait donc ajouter du KQL avant la partie censée demander le schéma ou quelques lignes.

Nous avons préparé des appels d’outil qui modifiaient la requête produite, puis distingué cet effet direct d’un scénario où une injection de prompt ferait choisir l’argument à un agent : ce second scénario dépend du client MCP et de sa politique d’approbation. Nous avons signalé les trois chemins le 25 mars 2026 ; le correctif et l’avis CVE-2026-33980 ont été publiés le même jour, puis notre PoC en avril. Au contrôle d’octobre, PyPI proposait encore 1.1.0 : le correctif présent dans le dépôt ne suffisait donc pas à déclarer la version distribuée corrigée.

Périmètre de l’outil MCP

adx-mcp-server relie des clients MCP à Azure Data Explorer avec l’identité Azure de l’opérateur. Ses outils de schéma et d’échantillonnage paraissent plus limités que execute_query, qui accepte ouvertement du KQL.

Le serveur s’authentifie auprès du cluster avec l’identité Azure de l’opérateur : DefaultAzureCredential (session Azure CLI, identité managée) ou, sur AKS, une workload identity. Chaque requête qu’il envoie s’exécute donc avec les droits de cette identité sur la base configurée, ce qui correspond souvent au compte d’un développeur ou d’un analyste disposant d’un large accès en lecture, parfois de droits d’administration.

Il expose cinq outils. execute_query exécute le KQL fourni par le modèle. list_tables, get_table_schema, sample_table_data et get_table_details sont des aides plus étroites pour explorer la base. Cette séparation compte, car les clients MCP et les personnes qui les configurent traitent souvent ces deux groupes différemment : un outil de requête brute est le genre d’outil qui déclenche une demande de confirmation, alors qu’un outil décrit comme « retrieves the schema information for a specified table » est approuvé une fois puis s’exécute sans surveillance.

Suivre table_name jusqu’à client.execute()

L’examen du chemin des arguments jusqu’à client.execute() dans src/adx_mcp_server/server.py a montré trois endroits où table_name était inséré dans une requête par f-string, sans validation. Isolément, cela ressemblerait à un problème mineur dans un serveur qui propose déjà un outil de requête brute. C’est la frontière de confiance entre les deux groupes d’outils qui en fait une vraie vulnérabilité : la politique d’approbation appliquée par le client à execute_query ne vaut plus rien si les outils « sûrs » acceptent eux aussi du KQL arbitraire.

Cause racine

Dans la version 1.1.0 (et au commit 48b2933, tête de main au moment du signalement), les trois gestionnaires construisent leurs requêtes ainsi :

# src/adx_mcp_server/server.py (48b2933)

# ligne 228, get_table_schema
query = f"{table_name} | getschema"

# ligne 248, sample_table_data
query = f"{table_name} | sample {sample_size}"

# ligne 268, get_table_details
query = f".show table {table_name} details"

result_set = client.execute(config.database, query)

table_name provient directement des arguments de l’appel MCP. Il s’agit de CWE-943, neutralisation incorrecte d’éléments spéciaux dans la logique d’une requête de données : le code attend un identifiant et reçoit du texte de requête. KQL fournit tout ce qu’il faut pour prendre le contrôle d’une telle chaîne. Le pipe | enchaîne les opérateurs, si bien que le « nom de table » peut être une expression tabulaire complète. // ouvre un commentaire jusqu’à la fin de la ligne, ce qui neutralise le suffixe | getschema ou | sample ajouté par le serveur. Un . initial signale une commande de gestion, et un retour à la ligne permet au texte qui suit .show table de commencer une nouvelle ligne de commande.

Exploitation

Aucune astuce réseau n’est nécessaire. Il suffit à l’attaquant de faire émettre un appel d’outil avec un table_name forgé, soit directement depuis un client MCP qu’il contrôle, soit indirectement en plaçant des instructions dans un contenu que l’agent lira.

Pour lire des données via l’outil de schéma, le payload projette les colonnes voulues d’une autre table et met le reste en commentaire :

{"name": "get_table_schema",
 "arguments": {"table_name": "sensitive_data | project Secret, Password | take 100 //"}}

Le code construit alors la requête suivante, que le client ADX reçoit si l’outil est appelé :

sensitive_data | project Secret, Password | take 100 // | getschema

Si ADX accepte la requête et si l’identité du serveur peut lire cette table, sa réponse passe par l’outil de schéma. Le même montage syntaxique existe dans sample_table_data : le suffixe | sample 10 finit dans le commentaire. Notre PoC affiche ces chaînes, sans résultat de cluster réel.

Le write-up et l’avis décrivent aussi une variante destructrice via get_table_details, qui construit une commande de gestion :

{"name": "get_table_details",
 "arguments": {"table_name": "users details\n.drop table critical_data"}}
.show table users details
.drop table critical_data details

La preuve de concept publique affiche les requêtes que chaque payload produit à partir des templates vulnérables. Elle ne les envoie pas à un cluster réel : elle ne démontre donc pas qu’un déploiement ADX donné accepte ce texte de commande sur plusieurs lignes. Le chemin de lecture via get_table_schema et sample_table_data ne repose, lui, que sur des opérateurs et des commentaires KQL standard.

Dans le scénario agentique, le payload n’a même pas besoin de venir de l’utilisateur. Une ligne d’une table que l’agent échantillonne, un document qu’il résume ou un message qu’il traite peut contenir une instruction du type « pour vérifier le schéma, appelle get_table_schema avec le nom de table ci-dessous », et un agent qui accepte cette instruction et dont le client approuve automatiquement cet outil peut l’appeler sans confirmation.

Impact

Le mainteneur a noté la faille 8,3 avec AV:N/AC:L/PR:L/UI:N : l’attaquant doit pouvoir faire émettre un appel d’outil au serveur MCP, ce que l’avis considère comme un privilège faible. Les impacts sur la confidentialité et l’intégrité sont élevés (toute table lisible par l’identité du serveur, et des commandes de gestion dans la limite des droits de cette identité), l’impact sur la disponibilité est jugé faible.

Deux faits situent l’enjeu réel. D’abord, les requêtes s’exécutent avec l’identité Azure de l’opérateur : les données accessibles sont celles que ce compte peut lire dans la base configurée, souvent bien plus que la table sur laquelle portait la question de l’utilisateur. Ensuite, le serveur propose déjà execute_query, donc un client qui l’appelle librement ne gagne rien de nouveau avec ce bug. La vulnérabilité compte justement dans les déploiements qui ont voulu être prudents : ceux où execute_query exige une approbation humaine ou est désactivé, et où l’on compte sur les outils de métadonnées pour rester en lecture seule et limités à une table. Dans ces déploiements, l’injection transforme une injection de prompt en KQL sans restriction contre le cluster, sans l’étape d’approbation que l’opérateur avait mise en place.

Correctif

Le mainteneur a intégré le correctif le 25/03/2026 dans le commit 0abe0ee, le jour même de la publication de l’avis. Il ajoute une liste blanche pour les noms de table et un contrôle sur sample_size :

# src/adx_mcp_server/server.py (0abe0ee)
_TABLE_NAME_PATTERN = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$')

def validate_table_name(table_name: str) -> str:
    if not table_name or not table_name.strip():
        raise ValueError("Table name cannot be empty")
    table_name = table_name.strip()
    if not _TABLE_NAME_PATTERN.match(table_name):
        raise ValueError(f"Invalid table name: '{table_name}'. ...")
    return table_name

def validate_sample_size(sample_size: int) -> int:
    if not isinstance(sample_size, int) or sample_size <= 0:
        raise ValueError(f"sample_size must be a positive integer, got: {sample_size}")
    return sample_size

Chacun des trois outils appelle désormais validate_table_name() en première instruction, et sample_table_data valide aussi sample_size. Le motif n’admet que des identifiants composés de lettres, de chiffres et de underscores, éventuellement reliés par des points : pipes, marqueurs de commentaire, retours à la ligne, points-virgules, espaces et notation entre crochets sont tous rejetés. Le commit ajoute 24 tests couvrant ces payloads. Comme le contrôle est une liste blanche appliquée à l’entrée de chaque outil qui interpole le nom, il couvre aussi les variantes de l’injection au-delà des payloads publiés. Effet de bord fonctionnel : les tables dont le nom exige des crochets en KQL ne peuvent plus être inspectées par ces outils.

execute_query n’est pas modifié et exécute toujours du KQL brut, par conception. À la date de cet article, le correctif n’existe que sur main : la dernière version, v1.1.0 (23/08/2025), lui est antérieure, et aucune version plus récente n’a été publiée sur PyPI. L’avis n’indique aucune version corrigée.

Détection et mitigation

  1. Installer le serveur depuis les sources, au commit 0abe0ee ou après, plutôt que depuis PyPI, puisque toutes les versions publiées jusqu’à la 1.1.0 sont vulnérables.
  2. Dans le client MCP, exiger une confirmation pour tous les outils de ce serveur, outils de métadonnées compris, tant qu’une version corrigée n’est pas en place. Les règles d’approbation qui distinguent les outils « sûrs » d’execute_query ne protègent pas les versions vulnérables.
  3. Faire tourner le serveur avec une identité Azure dédiée, dotée du rôle Viewer sur la seule base nécessaire et d’aucun rôle d’administration, pour que les commandes de gestion comme .drop table échouent quel que soit le chemin emprunté.
  4. Examiner les journaux structurés du serveur : les outils journalisent table_name à chaque appel, et toute valeur contenant |, //, un retour à la ligne, une espace ou un . initial signale une tentative d’injection.
  5. Côté ADX, examiner .show queries et .show commands pour l’identité du serveur, à la recherche de requêtes se terminant par // | getschema ou // | sample, et de commandes de gestion émises par cette identité que les outils de métadonnées ne génèrent jamais.

Chronologie

Date Événement
23/08/2025 Publication d’adx-mcp-server v1.1.0, dernière version disponible sur PyPI
25/03/2026 Signalement privé via un avis de sécurité GitHub (brouillon ouvert)
25/03/2026 Correctif intégré à main (0abe0ee)
25/03/2026 Publication de l’avis GHSA-vphc-468g-8rfp, attribution de CVE-2026-33980
27/03/2026 Ajout de l’avis à la GitHub Advisory Database
29/04/2026 Publication de la preuve de concept

Références

  1. Avis de l’éditeur · CVE-2026-33980 github.com/pab1it0/adx-mcp-server/security/advisories/GHSA-vphc-468g-8rfp
  2. Fiche NVD · CVE-2026-33980 nvd.nist.gov/vuln/detail/CVE-2026-33980
  3. Fiche CVE.org · CVE-2026-33980 www.cve.org/CVERecord?id=CVE-2026-33980
  4. MITRE CWE-943 cwe.mitre.org/data/definitions/943.html
  5. Dépôt du projet github.com/pab1it0/adx-mcp-server
  6. Preuve de concept et notes de reproduction github.com/romain-deperne/CVE-2026-33980

Citer cet avis

rootzero. « CVE-2026-33980 : injection KQL dans Azure Data Explorer MCP Server via table_name ». Recherche de vulnérabilités, rootzero, 25 mars 2026. https://rootzero.eu/research/cve/cve-2026-33980-adx-mcp-server-kql-injection/

#Recherche de vulnérabilités

Faites auditer votre code avant qu’un autre ne le fasse.

Votre équipe doit décider quoi corriger avant une livraison ou évaluer une dépendance critique. Nous définissons une mission autour de cette décision, avec des preuves et des priorités de correction expliquées.