CVE-2026-63720: Verified Reproduction
CVE-2026-63720: datamodel-code-generator code injection via customBasePath before 0.70.0
CVE-2026-63720 is verified against datamodel-code-generator · pip. Affected versions: >= 0.11.6, < 0.70.0. Fixed in 0.70.0. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00302.
What Is CVE-2026-63720?
CVE-2026-63720 is a high-severity vulnerability affecting datamodel-code-generator >= 0.11.6, < 0.70.0. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00302).
CVE-2026-63720 Severity
CVE-2026-63720 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
Affected datamodel-code-generator Versions
datamodel-code-generator · pip versions >= 0.11.6, < 0.70.0 are affected.
How to Reproduce CVE-2026-63720
pruva-verify REPRO-2026-00302 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00302/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-63720
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
Malicious JSON Schema customBasePath extension value containing a newline plus a dot-free Python expression (print(...) writing a marker file), supplied to the datamodel-codegen CLI.
- datamodel-codegen CLI --input malicious_schema.json --input-file-type jsonschema --output <generated.py>; importing the generated module executes the injected print() at module top level.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-63720
datamodel-code-generator (koxudaxi) before 0.70.0 contains a code-injection
vulnerability in its JSON Schema customBasePath extension handling. The
generator emits the schema-controlled customBasePath value verbatim into the
generated module's from <module> import <name> statement (and the model
base-class list) via Import.from_full_path, which splits the value on . and
performs no identifier validation. A malicious schema whose customBasePath
embeds a newline plus a dot-free Python expression causes attacker-controlled
Python statements to execute when a victim imports the generated module. The
vulnerability is reachable through the user-facing datamodel-codegen CLI
generator input-to-output path.
- Package/component affected:
datamodel_code_generatorparser (src/datamodel_code_generator/parser/jsonschema.py,JsonSchemaObject.custom_base_path/Import.from_full_path/Parser._resolve_base_class). - Affected versions: datamodel-code-generator before 0.70.0; vulnerable
checkout anchored at
af435d9893d1352924a2f011a2eb04a997d8b978(parent of the fix, reported as0.69.0). - Risk level: High. An attacker who supplies a JSON Schema to a victim running the generator achieves arbitrary Python code execution in the victim's import context when the generated module is imported. The generator output is the natural consumption path of the CLI, so the weaponized module is a direct product artifact.
Impact Parity
- Disclosed/claimed maximum impact: Arbitrary code execution (code injection on import of generated output).
- Reproduced impact from this run: Arbitrary attacker-controlled Python
code execution. Two fresh vulnerable
datamodel-codegenprocess instances generated modules whose import executed attacker-controlledprint(...)writing distinct marker files (DMCG_RCE_INST1,DMCG_RCE_INST2). The fixed build rejected the same schema with exit code 2 and produced no output and no marker. - Parity:
full. - Not demonstrated: None. The requested code-execution impact was demonstrated directly through the CLI entrypoint and the generated-module import consumption path.
Root Cause
JsonSchemaObject models the JSON Schema customBasePath extension as
custom_base_path: str | list[str] | None (alias customBasePath) with no
validation. The parser resolves a model's base class via
Parser._resolve_base_class(class_name, obj.custom_base_path), which returns
the raw custom_base_path string. That string is fed to
Import.from_full_path, which splits the value on . to derive a module path
and an import name, and the result is emitted verbatim into the generated
from <module> import <name> statement and the model base-class list. Because
the value is never checked to be a dotted Python identifier path, a value
containing a newline plus attacker-controlled Python text is written into the
generated module. Importing that module executes the injected text at module
top level.
A leading comma in the payload (pydantic.BaseModel,\n<expr>) is used so that
both the generated from pydantic import BaseModel,\n<expr> statement and the
class ExploitModel(BaseModel,\n<expr>): definition stay syntactically valid
(the newline is a continuation inside the comma-separated lists), letting the
module compile and the injected <expr> execute on import.
The fix commit 545a96c56d1b6a8dd3f4a16c9090d8a4648d1e43 ("Merge commit from
fork") adds a field_validator("custom_base_path", mode="before") that calls
_validate_schema_python_import_path (which delegates to
_validate_dotted_python_identifier_path) on every scalar/list member. Values
that are not dotted Python identifier paths raise Error: customBasePath must be a dotted Python identifier path: ..., the CLI exits with code 2, and no
output file is written.
Reproduction Steps
- Script:
bundle/repro/reproduction_steps.sh. - What the script does:
- Reads
bundle/project_cache_context.jsonand reuses the prepared repo at/pruva/project-cache/repowhenprepared=true; otherwise clones the repository. - Resolves the vulnerable commit
af435d98...(parent of fix) and the fixed commit545a96c5..., and verifies thevalidate_custom_base_pathvalidator is absent in the vulnerable checkout and present in the fixed checkout. - Creates two isolated git worktrees (one per commit) and editable-installs each into its own venv so the two builds cannot mutate each other's source tree.
- Generates a malicious JSON Schema whose
customBasePathispydantic.BaseModel,\nprint('<MARKER>', file=open('<path>','w'))(the injected expression is dot-free so it survivesImport.from_full_path's split-on-.). - Runs the real
datamodel-codegenCLI (--input ... --input-file-type jsonschema --output ...) on the schema for two fresh vulnerable process instances, then imports each generated module; the injectedprint()executes at module top level and writes a distinct marker file. - Runs the same CLI against the fixed build; it rejects the schema with exit code 2, writes no output, and produces no marker (negative control).
- Writes
bundle/repro/runtime_manifest.jsonandbundle/repro/negative_control_observation.json.
- Reads
- Expected evidence of reproduction: marker files
repro/marker_inst1(DMCG_RCE_INST1) andrepro/marker_inst2(DMCG_RCE_INST2) written by the vulnerable imports;repro/generated_vuln_inst1.pyshowing the injectedprint(...)at module level; fixed codegen log showing the rejection;negative_control_observation.jsonshowing no marker and exit code 2.
Evidence
bundle/logs/reproduction_steps.log— full run transcript (verdict CONFIRMED).bundle/logs/vuln_codegen_inst1.log,bundle/logs/vuln_codegen_inst2.log— vulnerable CLI codegen output (exit 0).bundle/logs/vuln_import_inst1.log,bundle/logs/vuln_import_inst2.log— generated-module import output (executes injected print, then TypeError from the None base class).bundle/logs/fixed_codegen.log— fixed CLI rejection (exit 2,customBasePath must be a dotted Python identifier path).bundle/repro/malicious_schema_inst1.json,bundle/repro/malicious_schema_inst2.json,bundle/repro/malicious_schema_fixed.json— attacker-controlled schemas.bundle/repro/generated_vuln_inst1.py— generated module containing the injectedprint('DMCG_RCE_INST1', file=open(...,'w'))at line 9.bundle/repro/marker_inst1=DMCG_RCE_INST1,bundle/repro/marker_inst2=DMCG_RCE_INST2— proof of executed attacker-controlled code.bundle/repro/negative_control_observation.json— fixed build: exit 2, no output, no marker.bundle/repro/runtime_manifest.json— runtime evidence manifest.
Key excerpt from generated_vuln_inst1.py:
from __future__ import annotations
from pydantic import BaseModel
print('DMCG_RCE_INST1', file=open('/workspace/bundle/repro/run/marker_inst1', 'w'))
class ExploitModel(
BaseModel,
print('DMCG_RCE_INST1', file=open('/workspace/bundle/repro/run/marker_inst1', 'w')),
):
value: str | None = None
Environment: CPython 3.14 on Linux x86_64; datamodel-code-generator 0.69.0 (vulnerable) and 0.69.1.dev1+g545a96c56 (fixed); pydantic v2.
Recommendations / Next Steps
- Upgrade guidance: Upgrade datamodel-code-generator to 0.70.0 or later,
which contains the
customBasePathfield_validator. - Suggested fix approach: The applied fix is appropriate: validate every
scalar and list member of
customBasePath(and the analogouscustomTypePath/x-python-importpaths) with_validate_dotted_python_identifier_pathbefore any code generation, and reject non-identifier values with a clear error and no output. Consider also hardening the generator's import/base-class emission to escape or reject control characters (newlines) in schema-controlled identifier fields as defense in depth. - Testing recommendations: Add regression tests that feed schemas with
newline-bearing and non-identifier
customBasePath/customTypePathvalues and assert the CLI exits non-zero with no output; the fix commit already addsunsafe_custom_base_path_scalar.json/unsafe_custom_base_path_list_nested.jsonfixtures for this.
Additional Notes
- Idempotency confirmation: The script was run twice consecutively; both
runs exited 0 with
CONFIRMED=1, both vulnerable instances wrote their distinct markers, and the fixed build rejected the schema both times. The script recreates worktrees/venvs each run, so it is self-cleaning. - Why the comma is required: Without the leading comma in the payload,
class ExploitModel(BaseModel\n<expr>):becomes aSyntaxError(two expressions with no separator inside the base-class list), which prevents the whole module from compiling and blocks execution of the injected statement. The comma keeps both the import and the class definition syntactically valid so the module compiles and the injected statement executes at import time; the subsequentTypeError(metaclass conflict from theNonereturned byprint) occurs after the marker is written. - Why the expression must be dot-free:
Import.from_full_pathsplits the rawcustomBasePathstring on., so any.in the injected text (including dots inside string literals) breaks the payload across segments and the final segment no longer contains the intended expression. Theprint(..., file=open(...))form is dot-free. - Surface match: The claim surface is
cli_local/cli_command. The reproduction exercises the realdatamodel-codegenCLI entrypoint (scripts.datamodel-codegen = "datamodel_code_generator.__main__:main") end-to-end and demonstrates the claimed code-execution impact through the generated-module import consumption path.
CVE-2026-63720 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Unknown error
ls -la /pruva/project-cache/ 2>&1 | head -50; echo "---"; ls -la /pruva/project-cache/repo 2>&1 | head -20; echo "---"; ls -la /pruva/project-cache/repo-mirrors 2>&1 | headEXPLOIT-KNOWLEDGE PREFLIGHT REVIEW REQUIRED: `run_shell` is blocked until this agent calls `review_exploit_knowledge_preflight` with snapshot_sha256 `7b2a523bb1bf53b1582f9c277ddec23bbc844a23d9bead643fad6a2bb302d8e0` and exactly one disposition for each candidate ID [33e92eae-aa49-41c0-b366-328175437821, 5172d9af-1f3d-4643-97dc-1080a146006c]. Historical records remain reference-only; review their typed mechanisms and preconditions before continuing.
Unknown error
Artifacts and Evidence for CVE-2026-63720
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-63720
Upgrade datamodel-code-generator · pip to 0.70.0 or later.
FAQ: CVE-2026-63720
Is CVE-2026-63720 exploitable?
How severe is CVE-2026-63720?
What type of vulnerability is CVE-2026-63720?
Which versions of datamodel-code-generator are affected by CVE-2026-63720?
Is there a fix for CVE-2026-63720?
How can I reproduce CVE-2026-63720?
Is the CVE-2026-63720 reproduction verified?
References for CVE-2026-63720
Authoritative sources for CVE-2026-63720 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.