CVE-2026-33625

CVSS 8.8 · Élevée Corrigé
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-33625 : exécution de code dans LMDeploy via eval() du quant_dtype d’un modèle

Un quantization_config piégé dans un modèle Hugging Face faisait passer quant_dtype à eval() dans LMDeploy : charger le modèle exécutait le code de l’attaquant.

Pour décider

Impact
La configuration d’un modèle piégé peut faire exécuter du code sur l’hôte d’inférence.
Conditions
Un opérateur doit charger le modèle concerné. Le code s’exécute avec les droits du processus LMDeploy.
Action recommandée
Passer à LMDeploy 0.12.3 ou ultérieur et contrôler la provenance des modèles chargés.

Fiche de l’avis

Produit
LMDeploy
Paquet
pypi · lmdeploy
Versions affectées
≥ 0.12.1, < 0.12.3
Corrigé dans
0.12.3
CVSS
8.8 Élevée
Faiblesse
CWE-95
Accès requis
Sans authentification
Avis publié
Crédit
rootzero

Synthèse de l’enquête

LMDeploy construisait une expression Python à partir du champ quant_dtype du quantization_config d’un modèle et la passait à eval() pendant le chargement de la configuration. Toute personne capable de publier un modèle qu’un opérateur charge ensuite, sur Hugging Face Hub ou un autre dépôt, pouvait exécuter du code arbitraire sur l’hôte d’inférence avec les privilèges du processus LMDeploy. Corrigé en 0.12.3.

Deux appels à eval(), un seul contrôlé par le modèle

Nous cherchions si la configuration d’un modèle non fiable pouvait atteindre un évaluateur Python pendant son chargement. Une recherche des appels à eval() dans le moteur PyTorch de LMDeploy a donné deux pistes. Nous avons écarté celle du dtype général : en mode automatique, la valeur était ramenée à une liste contrôlée. L’autre appel lisait quant_dtype directement dans quantization_config. C’était la frontière réelle entre le dépôt de modèle et le processus d’inférence.

Nous avons d’abord comparé les branches : fp8 remplaçait la valeur du modèle par un dtype fixe, alors que awq la conservait. Notre PoC local a donc créé une configuration Hugging Face simulée avec les champs AWQ et appelé le vrai chemin ModelConfig.from_hf_config(). Une expression tuple résolvait torch.float16 puis lançait une commande témoin dans son second élément. Sa sortie apparaissait avant toute lecture des poids ; nous n’avons pas eu besoin de publier ni de charger un dépôt malveillant complet pour prouver ce chemin d’évaluation. Nous avons transmis ce résultat borné aux mainteneurs le 11 mars 2026. Ils ont remplacé eval() par une résolution explicite du dtype, puis traité le repli AWQ ; la version 0.12.3 est sortie le 8 avril. L’avis public et CVE-2026-33625 n’ont suivi qu’en septembre. Le scénario suppose toujours qu’une personne ou un service charge un modèle non fiable.

Le fichier de configuration avant le chargement du modèle

LMDeploy accepte un répertoire local ou un dépôt Hugging Face. Son moteur PyTorch lit config.json avant l’inférence : un champ de configuration fait donc partie de la chaîne d’approvisionnement du modèle.

La chaîne d’approvisionnement des modèles a appris, en quelques années, quelles parties d’un dépôt de modèle sont dangereuses. Les poids sérialisés avec pickle exécutent du code à la désérialisation, d’où le passage à safetensors. Les fichiers Python livrés avec un modèle ne s’exécutent que si l’opérateur passe trust_remote_code. La configuration JSON est la partie que tout le monde considère comme une donnée inerte : elle décrit l’architecture, le nombre de couches, le dtype et, pour les modèles quantifiés, un bloc quantization_config. Un opérateur qui refuse les pickles et le code distant charge malgré tout le config.json de quiconque a publié le modèle. C’est cette hypothèse que la vulnérabilité met en défaut.

L’appel à eval() qui passe les contrôles de branche

Une recherche de eval( dans le moteur PyTorch de LMDeploy renvoie deux occurrences dans lmdeploy/pytorch/config.py. La première, dans _update_torch_dtype(), convertit le dtype du modèle, mais la valeur évaluée est soit l’argument dtype fourni par l’opérateur, soit, en mode auto, une valeur ramenée de force à float16 ou bfloat16. Un modèle ne peut pas l’orienter. La seconde, dans QuantizationConfig.from_config(), convertit quant_dtype, et en remontant cette variable on constate qu’elle provient directement du quantization_config du modèle, sans aucun contrôle en chemin.

Cause racine

Le code vulnérable, tel qu’il se présente en 0.12.2 (permalien sur 17ed9e5) :

# lmdeploy/pytorch/config.py (0.12.2), QuantizationConfig.from_config()
quant_method = quant_config['quant_method']
quant_dtype = quant_config.get('quant_dtype', None)
...
if quant_method == 'awq':
    bits = quant_config.get('bits', 4)
    group_size = quant_config.get('group_size', 128)
elif quant_method == 'smooth_quant':
    if quant_dtype is None:
        quant_dtype = 'int8'
elif quant_method == 'fp8':
    fmt = quant_config.get('fmt', 'e4m3')
    if fmt == 'e4m3':
        quant_dtype = 'float8_e4m3fn'
    elif fmt == 'e5m2':
        quant_dtype = 'float8_e5m2'
    else:
        raise TypeError(f'Unsupported fp8 fmt: {fmt}')
else:
    raise TypeError(f'Unsupported quant method: {quant_method}')

if quant_dtype is not None:
    quant_dtype = eval(f'torch.{quant_dtype}')

quant_config est le dictionnaire quantization_config de la configuration Hugging Face (ou de sa sous-configuration llm_config ou text_config pour les modèles multimodaux). La branche fp8 remplace quant_dtype par une chaîne fixe et reste donc sûre. La branche awq ne touche jamais à quant_dtype, et la branche smooth_quant ne le renseigne que lorsqu’il est absent. Dans ces deux cas, la chaîne écrite par l’auteur du modèle atteint eval() telle quelle.

Il s’agit de CWE-95, la neutralisation incorrecte de directives dans du code évalué dynamiquement (eval injection). L’avis du dépôt indique bien CWE-95 ; l’entrée de la GitHub Advisory Database mentionne CWE-400 (consommation non contrôlée de ressources), qui décrit une autre classe de défaut et semble être une erreur d’étiquetage.

QuantizationConfig.from_config() est appelée sans condition pendant que LMDeploy construit son ModelConfig, avant la lecture du moindre poids et indépendamment de trust_remote_code. Le défaut apparaît en 0.12.1 : l’évaluation de quant_dtype est absente du config.py de la 0.12.0, ce qui concorde avec la plage de versions affectées de l’avis.

Exploitation

L’attaquant publie un dépôt de modèle dont le config.json déclare une quantification AWQ et un quant_dtype qui est une expression Python :

{
  "quantization_config": {
    "quant_method": "awq",
    "quant_dtype": "float16, __import__('os').system('id')"
  }
}

LMDeploy évalue alors la ligne suivante :

eval("torch.float16, __import__('os').system('id')")

Python l’interprète comme un tuple, une expression parfaitement valide. Le premier élément donne le dtype attendu, le second importe os et lance la commande shell pendant la construction du tuple. Quelle que soit la suite du chargement, la commande a déjà été exécutée. Un attaquant réel garderait le reste de la configuration valide (architecture, nombre de têtes, poids) pour que le modèle se charge et serve normalement une fois le payload exécuté.

Il suffit que l’opérateur désigne le dépôt à LMDeploy :

lmdeploy serve api_server attacker/awesome-llm-awq

La preuve de concept du signalement emprunte le vrai chemin de code, dans le même processus : elle construit une fausse configuration Hugging Face portant le payload, avec des valeurs plausibles pour le nombre de têtes, la taille cachée et le vocabulaire, puis appelle ModelConfig.from_hf_config(). La sortie de la commande (uid=0(root) gid=0(root) groups=0(root)) apparaît sans qu’aucun poids de modèle ne soit en jeu.

Impact

L’avis note la faille 8,8 avec AV:N/AC:L/PR:N/UI:R. L’attaquant n’a besoin d’aucun compte sur la cible : publier un modèle sur un hub public suffit, d’où un vecteur réseau sans privilège requis. Une interaction utilisateur est nécessaire, car une personne ayant accès à l’hôte d’inférence doit choisir de charger ce modèle. Le signalement d’origine parle d’« aucune interaction au-delà du lancement de lmdeploy serve », ce qui décrit le même fait du point de vue de l’opérateur : charger le modèle est la seule interaction, et c’est l’usage normal de l’outil.

Le code s’exécute avec les privilèges du processus LMDeploy, avec un impact total sur la confidentialité, l’intégrité et la disponibilité. Concrètement, un hôte d’inférence est une position intéressante : de la capacité GPU, les poids des modèles qu’il sert (parfois des fine-tunes propriétaires), le jeton Hugging Face utilisé pour récupérer des modèles privés, et les identifiants cloud que le déploiement lui confie. Le payload peut aussi altérer le modèle sur le point d’être servi, ce qui se remarque bien moins qu’un serveur tombé.

L’exigence d’interaction s’affaiblit dès que le choix du modèle est délégué. Une plateforme qui laisse ses utilisateurs demander un modèle par identifiant de dépôt, ou une chaîne d’évaluation qui récupère automatiquement de nouveaux checkpoints quantifiés, transforme « un opérateur charge un modèle » en « un attaquant soumet un identifiant de dépôt », et l’attaque n’exige alors plus aucune décision humaine.

Correctif

LMDeploy 0.12.3, publié le 08/04/2026, corrige le défaut. La modification se trouve dans la pull request #4447 (« fix security issues », commit de fusion 71d64a3), qui remplace les deux appels à eval() de config.py par une recherche d’attribut suivie d’un contrôle de type :

# lmdeploy/pytorch/config.py (0.12.3), QuantizationConfig.from_config()
resolved_quant_dtype = getattr(torch, quant_dtype, None)
if not isinstance(resolved_quant_dtype, torch.dtype):
    raise ValueError(f'Invalid quant dtype "{quant_dtype}" resolved from model config; '
                     'expected a torch.dtype attribute on torch.')
quant_dtype = resolved_quant_dtype

getattr() traite la chaîne comme un nom d’attribut et ne l’interprète jamais comme du code, et le contrôle isinstance(..., torch.dtype) rejette les attributs de torch qui existent sans être des dtypes, comme des fonctions ou des sous-modules. Le même motif protège désormais config.dtype dans _update_torch_dtype() : le second site d’évaluation est donc fermé lui aussi, bien qu’un modèle ne puisse pas l’atteindre.

La réécriture a supprimé l’ancienne garde if quant_dtype is not None, si bien qu’un modèle AWQ ordinaire sans champ quant_dtype aurait passé None à getattr(). La pull request #4503 (« Fix torch awq », commit 2539582), fusionnée la veille de la publication, définit une valeur de substitution bfloat16 dans la branche AWQ lorsque le champ manque. La PR #4447 ajoute aussi un contrôle qui bloque les adresses IP non globales quand le pipeline de vision télécharge un média depuis une URL. Ce changement traite une SSRF et n’a pas de lien avec cette CVE.

Détection et mitigation

  1. Mettre à jour LMDeploy vers la 0.12.3 ou une version ultérieure sur chaque hôte et dans chaque image de conteneur qui charge des modèles, y compris les tâches d’évaluation et de quantification qui ne servent pas de trafic.

  2. Traiter un dépôt de modèle comme du code tant qu’il n’a pas été relu. Charger les modèles depuis un miroir interne ou une révision épinglée (un hash de commit) inspectée par quelqu’un, et éviter de laisser des utilisateurs finaux ou des tâches automatiques choisir des identifiants de dépôt arbitraires.

  3. Rechercher les configurations piégées dans le cache Hugging Face local et dans les répertoires de modèles. Un quant_dtype légitime est un simple nom de dtype torch ; toute valeur contenant d’autres caractères mérite un examen :

    find ~/.cache/huggingface/hub /models -name config.json -exec \
      jq -r '.. | objects | select(has("quant_dtype")) | "\(input_filename): \(.quant_dtype)"' {} + \
      | grep -Ev ': [a-z0-9_]+$'
  4. Exécuter les serveurs d’inférence sous un utilisateur non privilégié, avec un stockage de modèles en lecture seule, aucun identifiant cloud au-delà de ce que le service exige, et un accès réseau sortant limité au registre de modèles. Définir HF_HUB_OFFLINE=1 sur les serveurs de production qui ne doivent jamais télécharger de nouveau modèle au démarrage.

  5. Surveiller la télémétrie des processus pour repérer les shells ou utilitaires système (sh, bash, curl, wget) dont le parent est le processus Python qui exécute LMDeploy, par exemple avec une règle auditd ou Falco sur execve. Un chargement de modèle n’a aucune raison d’en lancer.

Chronologie

Date Événement
13/02/2026 Publication de LMDeploy 0.12.1, première version affectée
11/03/2026 Signalement privé aux mainteneurs via un avis de sécurité GitHub
18/03/2026 Publication de LMDeploy 0.12.2, toujours affectée
27/03/2026 Fusion de la pull request #4447 (71d64a3), eval() remplacé par getattr()
07/04/2026 Fusion de la pull request #4503 (2539582), dtype de substitution pour AWQ
08/04/2026 Publication de LMDeploy 0.12.3
16/09/2026 Publication de l’avis GHSA-3hmm-rh5q-gwwr, attribution de CVE-2026-33625
18/09/2026 Intégration de l’avis, après revue, dans la GitHub Advisory Database

Références

  1. Avis de l’éditeur · CVE-2026-33625 github.com/InternLM/lmdeploy/security/advisories/GHSA-3hmm-rh5q-gwwr
  2. Fiche NVD · CVE-2026-33625 nvd.nist.gov/vuln/detail/CVE-2026-33625
  3. Fiche CVE.org · CVE-2026-33625 www.cve.org/CVERecord?id=CVE-2026-33625
  4. MITRE CWE-95 cwe.mitre.org/data/definitions/95.html
  5. Dépôt du projet github.com/InternLM/lmdeploy

Citer cet avis

rootzero. « CVE-2026-33625 : exécution de code dans LMDeploy via eval() du quant_dtype d’un modèle ». Recherche de vulnérabilités, rootzero, 16 septembre 2026. https://rootzero.eu/research/cve/cve-2026-33625-lmdeploy-eval-rce/

#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.