
Code injection (RCE) in datamodel-code-generator via unvalidated customBasePath (CVE-2026-63720)
Severity: High, CVSS 3.1 7.5 / CVSS 4.0 7.5 (assigned by VulnCheck, the CNA)
Environmental ceiling (network-service deployment): up to 9.8
Vector (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
Vector (v3.1): CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
Affected: datamodel-code-generator < 0.70.0
Fixed in: 0.70.0
CWE: CWE-94 (Improper Control of Generation of Code, 'Code Injection')
Reported by: Rahul Karne
CNA: VulnCheck
Published: July 26, 2026
datamodel-code-generator validated every schema-controlled import string that could carry a code-injection payload, except one.
The tool converts an input schema (JSON Schema, OpenAPI, YAML) into Python model
source. Several schema fields are rendered directly into that generated code, so
the project validates them as dotted Python identifiers before use, specifically
to prevent injection. The schema extension field customBasePath is the one
sibling that skips this check. Its value flows unsanitized into a
from ... import ... statement in the generated output. An attacker who controls
the input schema can embed arbitrary Python using newlines and a dot-free
expression, and it executes the moment the generated module is imported, which
is the ordinary next step after generating models.
This is an incomplete fix of CVE-2026-55415 (GHSA-5578-w22f-pfx9), which
hardened the sibling fields customTypePath and x-python-import against this
exact class. That fix did not cover customBasePath, which reaches the identical
sink unvalidated and remained exploitable through 0.68.1 and main until 0.70.0.
Arbitrary Python code execution in the process that imports or runs the generated models: the developer's machine, a CI runner, or any service that generates and then loads models. Confidentiality, integrity, and availability of the host are fully compromised, bounded only by the privileges of that process.
The severity depends entirely on where codegen runs on untrusted input:
Who is affected: Any use of datamodel-code-generator < 0.70.0 that (1)
generates models from a schema whose customBasePath value is attacker
influenced, and (2) imports or executes the generated module. The default
codegen-then-import workflow satisfies (2) inherently.
Who is not affected:
0.70.0 or later, where customBasePath is validated.| Metric | Value | Source |
|---|---|---|
| Downloads, all-time | 194 Million | pepy.tech/projects/datamodel-code-generator |
| Downloads, last 30 days | 16.3 Million | pepy.tech |
| Typical deployment | Developer machines, CI/CD pipelines, and SDK-generation platforms that codegen from OpenAPI / JSON Schema | inherent to the tool's function |
The value of the customBasePath schema field is carried into generated code
with no identifier constraint. Three points in the codebase matter (paths
relative to src/datamodel_code_generator/):
parser/jsonschema.py defines the field
custom_base_path with alias="customBasePath" (~line 644), consumed via
_resolve_base_class(...) at several call sites.parser/base.py, _resolve_base_class (~line 1665),
returns the value after only a local normalize() (dedup/strip). No identifier
validation is applied.imports.py, Import.from_full_path() (~line 35), emits the value
verbatim as a from ... import ... line. The value is also used as the class
base in model/base.py set_base_class (~line 1324) and rendered raw by the
model template (class {{ class_name }}({{ base_class }}):).Because the value is written into Python source with no constraint, embedded newlines and a dot-free expression survive into the output as their own individually parseable lines, and the middle line executes on import.
The payload is dot-free by necessity. Import.from_full_path splits the value on
., so a normal os.system(...) call would be broken apart. Using
getattr(__import__('os'),'system')(...) avoids any . while still resolving the
same call, and the surrounding newlines keep the emitted from ... import ...
lines syntactically valid so the injected middle line runs cleanly.
This is not a project that neglected injection. The maintainer hardened this
exact class repeatedly across multiple advisories (GHSA-5578, m34r, 8m8r, wjv6),
each time routing a schema-controlled import or type string through
_validate_dotted_python_identifier_path before it reaches code generation. The
sibling fields customTypePath (validated at parser/jsonschema.py ~lines 4956,
5202) and x-python-import (~line 2096) both go through that validator.
customBasePath is the one sibling with no such call. It reaches the same
Import.from_full_path sink by a different path (_resolve_base_class) that was
never wired into the validation the other fields received. The defect survived
precisely because the surrounding defense looked complete: a reviewer scanning
for unvalidated import strings sees validators on the fields they check first,
and this one routes through a helper that looks like base-class resolution rather
than import handling. It is a gap in a systematic fix, not an absent one, which
is why it persisted into the latest release.
An attacker needs:
< 0.70.0.customBasePath value in a schema the target will process,
in practice by supplying or influencing the input schema (a third-party
OpenAPI/JSON Schema document, or a schema submitted to a service).No authentication or elevated privileges are required of the attacker (PR:N).
The base score reflects that the victim performs the ordinary generate-and-import
action (UI:R in v3.1 / UI:A in v4.0); the network-service deployment removes
even that, which is where the environmental 9.8 comes from.
The following was run against the real, unmodified package. Reproduction:
pip install "datamodel-code-generator==0.68.1"
datamodel-codegen --input attack.json --input-file-type jsonschema --output generated_models.py
python -c "import generated_models"
Attacker input (attack.json):
{
"type": "object",
"title": "User",
"customBasePath": "builtins import object\ngetattr(__import__('os'),'system')('whoami > RCE_PROOF.txt')\nfrom builtins.object",
"properties": { "name": { "type": "string" } }
}
Generated generated_models.py on the vulnerable version (0.68.1):