
Injection de code (RCE) dans datamodel-code-generator via customBasePath non validé (CVE-2026-63720)
Sévérité : Élevée, CVSS 3.1 7.5 / CVSS 4.0 7.5 (attribué par VulnCheck, l'organisme CNA)
Plafond environnemental (déploiement en service réseau) : jusqu'à 9.8
Vecteur (v4.0) : CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Vecteur (v3.1) : CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
Versions concernées : datamodel-code-generator < 0.70.0
Corrigé dans : 0.70.0
CWE : CWE-94 (Contrôle inadéquat de la génération de code, « Injection de code »)
Signalé par : Rahul Karne
CNA : VulnCheck
Publié : 26 juillet 2026
datamodel-code-generator validait chaque chaîne d'importation contrôlée par le schéma qui pouvait transporter une charge utile d'injection de code, sauf une.
L'outil convertit un schéma d'entrée (JSON Schema, OpenAPI, YAML) en code source de modèles Python. Plusieurs champs du schéma sont rendus directement dans ce code généré, c'est pourquoi le projet les valide comme identifiants Python en pointillés avant utilisation, précisément pour empêcher l'injection. Le champ d'extension de schéma customBasePath est le seul champ apparenté qui contourne cette vérification. Sa valeur circule sans assainissement dans une instruction from ... import ... à la sortie générée. Un attaquant qui contrôle le schéma d'entrée peut intégrer du code Python arbitraire à l'aide de sauts de ligne et d'une expression sans point, et ce code s'exécute au moment où le module généré est importé, ce qui est l'étape suivante normale après la génération des modèles.
Il s'agit d'un correctif incomplet de CVE-2026-55415 (GHSA-5578-w22f-pfx9), qui avait durci les champs apparentés customTypePath et x-python-import contre cette classe d'attaque exacte. Ce correctif ne couvrait pas customBasePath, qui atteint le même point de chute non validé et restait exploitable jusqu'à la version 0.68.1 et sur la branche main jusqu'à la version 0.70.0.
Exécution de code Python arbitraire dans le processus qui importe ou exécute les modèles générés : la machine du développeur, un exécuteur CI, ou tout service qui génère puis charge des modèles. La confidentialité, l'intégrité et la disponibilité de l'hôte sont entièrement compromises, limitées uniquement par les privilèges de ce processus.
La sévérité dépend entièrement de l'endroit où la génération de code s'exécute sur des entrées non fiables :
Qui est concerné : Toute utilisation de datamodel-code-generator < 0.70.0 qui (1) génère des modèles à partir d'un schéma dont la valeur customBasePath est influencée par un attaquant, et (2) importe ou exécute le module généré. Le flux de travail par défaut génération-puis-importation satisfait (2) de manière inhérente.
Qui n'est pas concerné :
0.70.0 ou une version ultérieure, où customBasePath est validé.| Métrique | Valeur | Source |
|---|---|---|
| Téléchargements, tous temps confondus | 194 millions | pepy.tech/projects/datamodel-code-generator |
| Téléchargements, 30 derniers jours | 16,3 millions | pepy.tech |
| Déploiement typique | Machines de développeurs, pipelines CI/CD et plateformes de génération de SDK qui génèrent du code à partir d'OpenAPI / JSON Schema | inhérent à la fonction de l'outil |
La valeur du champ de schéma customBasePath est transportée dans le code généré sans contrainte d'identifiant. Trois points du code source importent (chemins relatifs à src/datamodel_code_generator/) :
parser/jsonschema.py définit le champ custom_base_path avec alias="customBasePath" (~ligne 644), consommé via _resolve_base_class(...) à plusieurs endroits d'appel.parser/base.py, _resolve_base_class (~ligne 1665), renvoie la valeur après seulement un normalize() local (déduplication/suppression des espaces). Aucune validation d'identifiant n'est appliquée.imports.py, Import.from_full_path() (~ligne 35), émet la valeur telle quelle sous forme de ligne from ... import .... La valeur est également utilisée comme classe de base dans model/base.py set_base_class (~ligne 1324) et rendue brute par le modèle de template (class {{ class_name }}({{ base_class }}):).Comme la valeur est écrite dans le code source Python sans contrainte, les sauts de ligne intégrés et une expression sans point survivent dans la sortie sous forme de lignes individuelles analysables, et la ligne du milieu s'exécute à l'importation.
La charge utile est sans point par nécessité. Import.from_full_path divise la valeur sur ., donc un appel os.system(...) normal serait décomposé. Utiliser getattr(__import__('os'),'system')(...) évite tout . tout en résolvant le même appel, et les sauts de ligne environnants maintiennent les lignes from ... import ... émises syntaxiquement valides, de sorte que la ligne du milieu injectée s'exécute proprement.
Ce n'est pas un projet qui a négligé l'injection. Le mainteneur a durci cette classe d'attaque exacte à plusieurs reprises à travers de multiples avis (GHSA-5578, m34r, 8m8r, wjv6), en faisant passer à chaque fois une chaîne d'importation ou de type contrôlée par le schéma par _validate_dotted_python_identifier_path avant qu'elle n'atteigne la génération de code. Les champs apparentés customTypePath (validé à parser/jsonschema.py ~lignes 4956, 5202) et x-python-import (~ligne 2096) passent tous deux par ce validateur.
customBasePath est le seul champ apparenté sans un tel appel. Il atteint le même point de chute Import.from_full_path par un chemin différent (_resolve_base_class) qui n'a jamais été câblé dans la validation que les autres champs ont reçue. Le défaut a survécu précisément parce que la défense environnante semblait complète : un relecteur recherchant des chaînes d'importation non validées voit des validateurs sur les champs qu'il vérifie en premier, et celui-ci passe par une fonction d'assistance qui ressemble à une résolution de classe de base plutôt qu'à un traitement d'importation. C'est une lacune dans un correctif systématique, et non une absence de correctif, ce qui explique pourquoi elle a persisté jusqu'à la dernière version.
Un attaquant a besoin :