# REPRO-2026-00302: datamodel-code-generator code injection via customBasePath before 0.70.0 ## Summary Status: published Severity: high CVSS: Unknown CWE: CWE-94 Improper Control of Generation of Code ('Code Injection') (Improper Control of Generation of Code ('Code Injection')) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00302 CVE: CVE-2026-63720 ## Package Name: datamodel-code-generator Ecosystem: pip Affected: >= 0.11.6, < 0.70.0 Fixed: 0.70.0 ## Root Cause # RCA Report — CVE-2026-63720 ## Summary 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 import ` 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. ## Impact - **Package/component affected:** `datamodel_code_generator` parser (`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 as `0.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-codegen` process instances generated modules whose import executed attacker-controlled `print(...)` 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 import ` 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`) is used so that both the generated `from pydantic import BaseModel,\n` statement and the `class ExploitModel(BaseModel,\n):` definition stay syntactically valid (the newline is a continuation inside the comma-separated lists), letting the module compile and the injected `` 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 1. Script: `bundle/repro/reproduction_steps.sh`. 2. What the script does: - Reads `bundle/project_cache_context.json` and reuses the prepared repo at `/pruva/project-cache/repo` when `prepared=true`; otherwise clones the repository. - Resolves the vulnerable commit `af435d98...` (parent of fix) and the fixed commit `545a96c5...`, and verifies the `validate_custom_base_path` validator 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 `customBasePath` is `pydantic.BaseModel,\nprint('', file=open('','w'))` (the injected expression is dot-free so it survives `Import.from_full_path`'s split-on-`.`). - Runs the real `datamodel-codegen` CLI (`--input ... --input-file-type jsonschema --output ...`) on the schema for two fresh vulnerable process instances, then imports each generated module; the injected `print()` 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.json` and `bundle/repro/negative_control_observation.json`. 3. Expected evidence of reproduction: marker files `repro/marker_inst1` (`DMCG_RCE_INST1`) and `repro/marker_inst2` (`DMCG_RCE_INST2`) written by the vulnerable imports; `repro/generated_vuln_inst1.py` showing the injected `print(...)` at module level; fixed codegen log showing the rejection; `negative_control_observation.json` showing 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 injected `print('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`: ```python 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 `customBasePath` field_validator. - **Suggested fix approach:** The applied fix is appropriate: validate every scalar and list member of `customBasePath` (and the analogous `customTypePath` / `x-python-import` paths) with `_validate_dotted_python_identifier_path` before 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`/`customTypePath` values and assert the CLI exits non-zero with no output; the fix commit already adds `unsafe_custom_base_path_scalar.json` / `unsafe_custom_base_path_list_nested.json` fixtures 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):` becomes a `SyntaxError` (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 subsequent `TypeError` (metaclass conflict from the `None` returned by `print`) occurs after the marker is written. - **Why the expression must be dot-free:** `Import.from_full_path` splits the raw `customBasePath` string 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. The `print(..., file=open(...))` form is dot-free. - **Surface match:** The claim surface is `cli_local` / `cli_command`. The reproduction exercises the real `datamodel-codegen` CLI 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. ## Reproduction Details Reproduced: 2026-07-27T05:42:55.896Z Duration: 824 seconds Tool calls: 227 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00302 pruva-verify CVE-2026-63720 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00302&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00302/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-63720 - Source: https://github.com/koxudaxi/datamodel-code-generator ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 12637 bytes) - bundle/repro/rca_report.md (analysis, 10074 bytes) - bundle/repro/runtime_manifest.json (other, 1351 bytes) - bundle/logs/reproduction_steps.log (log, 6123 bytes) - bundle/logs/vuln_codegen_inst2.log (log, 429 bytes) - bundle/logs/vuln_import_inst2.log (log, 398 bytes) - bundle/repro/malicious_schema_inst2.json (other, 299 bytes) - bundle/repro/generated_vuln_inst2.py (script, 429 bytes) - bundle/repro/malicious_schema_fixed.json (other, 314 bytes) - bundle/repro/validation_verdict.json (other, 1057 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00302 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00302/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/reproductions/REPRO-2026-00302 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev