PYSEC-2026-3564

Vulnerability from pysec - Published: 2026-08-04 11:34 - Updated: 2026-08-04 13:36
VLAI
Details

Summary

datamodel-code-generator's built-in HTTP fetcher (http.get_body) issues an httpx.GET against any URL passed to --url (or reached via a redirect chain) with no allow-list, no deny-list, no IP/host validation, and follow_redirects=True. Loopback addresses, RFC1918 ranges, link-local (169.254.169.254 cloud metadata), unique-local IPv6 and any other network-accessible target are all reachable. The JSON/YAML response body is parsed as a schema and reflected into the generated .py source, exfiltrating the response to anyone with access to that file (commonly committed to a repository).

Details

Sink: src/datamodel_code_generator/http.py, get_body (lines 31–61, at tag 0.60.1 / commit a321547e):

def get_body(url, headers=None, ignore_tls=False,
             query_parameters=None, timeout=DEFAULT_HTTP_TIMEOUT) -> str:
    httpx = _get_httpx()
    try:
        response = httpx.get(
            url,
            headers=headers,
            verify=not ignore_tls,
            follow_redirects=True,          # (A)
            params=query_parameters,
            timeout=timeout,
        )
    except Exception as e:
        ...
    if response.status_code >= 400:
        ...
    content_type = response.headers.get("content-type", "").lower()
    if "text/html" in content_type:
        raise SchemaFetchError(...)         # (B) — only filter
    return response.text                    # (C) → embedded in generated.py
  • (A) follows redirects unconditionally — a public URL → 302 → internal address chain works.
  • (B) the only filter is rejecting text/html. Non-HTML internal endpoints (JSON APIs, cloud metadata, admin services) pass through.
  • (C) the response body becomes the schema; its title, description, properties, etc. land in the generated .py as class attributes and Field(description=...) strings.

get_body is called by parser/base.py:1326 (_get_text_from_url), which is reached from CLI argument --url <URL>. (The $ref path is a separate advisory — see GHSA-D.)

Only affects users who installed the [http] extra (pip install 'datamodel-code-generator[http]').

PoC

A self-contained one-file PoC available here: https://gist.github.com/thegr1ffyn/18de777d6c800a3b47715425e3f3e8f5

Impact

Who is impacted. Anyone who runs datamodel-codegen with a --url they didn't fully verify, or who runs it inside a network with reachable internal services. Realistic scenarios:

  1. Trojan documentation / README. A blog post or README example reads datamodel-codegen --url https://schemas.example.com/user.json -o user.py. The attacker controls example.com, redirects to http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>, and the IAM credentials end up as a docstring in user.py.
  2. Internal port scan / disclosure. Iterating --url http://127.0.0.1:<port>/health probes localhost services; non-HTML, non-error responses confirm a service and leak its body into the generated file.
  3. CI poisoning. A PR adds a Makefile rule that calls datamodel-codegen --url $(SCHEMA_URL); the CI runner reaches every internal service in its VPC and the response lands in PR artifacts.

Suggested fix. Resolve the URL host, reject loopback / private / link-local / multicast / reserved IPs by default, disable redirects by default (follow_redirects=False), re-validate after each redirect if the user opts into following them, and add an --allow-private-network flag for opt-in legitimate use.

Maintainer resolution

This report was fixed together with GHSA-954p-556p-r752 by the private security PR koxudaxi/datamodel-code-generator-ghsa-rfr2-mq9m-x2qx#1, merged into the public repository as 5fdba4a09f2d7a9996a504975b7ef7d63e3715bb. Follow-up generated-file and coverage fixes were merged in koxudaxi/datamodel-code-generator#3279 and docs were synced in #3280. The patched release is 0.61.0.

The patch hardens the shared HTTP fetcher used by both direct CLI --url fetching and remote JSON Schema/OpenAPI $ref resolution:

  • validates HTTP(S) URLs before fetching;
  • blocks localhost, loopback, private, link-local, reserved, and other non-public network targets by default;
  • disables automatic redirect following and validates each redirect target before requesting it;
  • adds --allow-private-network / allow_private_network=True as an explicit opt-in for trusted internal schema endpoints.

Remote $ref fetching remains controlled by --allow-remote-refs; non-public/internal targets additionally require --allow-private-network.

Submitted by: Hamza Haroon (thegr1ffyn)

Impacted products
Name purl
datamodel-code-generator pkg:pypi/datamodel-code-generator

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "datamodel-code-generator",
        "purl": "pkg:pypi/datamodel-code-generator"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.9.1"
            },
            {
              "fixed": "0.61.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "0.10.0",
        "0.10.1",
        "0.10.2",
        "0.10.3",
        "0.11.0",
        "0.11.1",
        "0.11.10",
        "0.11.11",
        "0.11.12",
        "0.11.13",
        "0.11.14",
        "0.11.15",
        "0.11.16",
        "0.11.17",
        "0.11.18",
        "0.11.19",
        "0.11.2",
        "0.11.20",
        "0.11.3",
        "0.11.4",
        "0.11.5",
        "0.11.6",
        "0.11.7",
        "0.11.8",
        "0.11.9",
        "0.12.0",
        "0.12.1",
        "0.12.2",
        "0.12.3",
        "0.13.0",
        "0.13.1",
        "0.13.2",
        "0.13.3",
        "0.13.4",
        "0.13.5",
        "0.14.0",
        "0.14.1",
        "0.15.0",
        "0.16.0",
        "0.16.1",
        "0.17.0",
        "0.17.1",
        "0.17.2",
        "0.18.0",
        "0.18.1",
        "0.19.0",
        "0.20.0",
        "0.21.0",
        "0.21.1",
        "0.21.2",
        "0.21.3",
        "0.21.4",
        "0.21.5",
        "0.22.0",
        "0.22.1",
        "0.23.0",
        "0.24.0",
        "0.24.1",
        "0.24.2",
        "0.25.0",
        "0.25.1",
        "0.25.2",
        "0.25.3",
        "0.25.4",
        "0.25.5",
        "0.25.6",
        "0.25.7",
        "0.25.8",
        "0.25.9",
        "0.26.0",
        "0.26.1",
        "0.26.2",
        "0.26.3",
        "0.26.4",
        "0.26.5",
        "0.27.0",
        "0.27.1",
        "0.27.2",
        "0.27.3",
        "0.28.0",
        "0.28.1",
        "0.28.2",
        "0.28.3",
        "0.28.4",
        "0.28.5",
        "0.29.0",
        "0.30.0",
        "0.30.1",
        "0.30.2",
        "0.31.0",
        "0.31.1",
        "0.31.2",
        "0.32.0",
        "0.33.0",
        "0.34.0",
        "0.35.0",
        "0.36.0",
        "0.37.0",
        "0.38.0",
        "0.39.0",
        "0.40.0",
        "0.41.0",
        "0.42.0",
        "0.42.1",
        "0.42.2",
        "0.43.0",
        "0.43.1",
        "0.44.0",
        "0.45.0",
        "0.46.0",
        "0.47.0",
        "0.48.0",
        "0.49.0",
        "0.50.0",
        "0.51.0",
        "0.52.0",
        "0.52.1",
        "0.52.2",
        "0.53.0",
        "0.54.0",
        "0.54.1",
        "0.55.0",
        "0.56.0",
        "0.56.1",
        "0.57.0",
        "0.58.0",
        "0.59.0",
        "0.59.1",
        "0.60.0",
        "0.60.1",
        "0.60.2",
        "0.9.1",
        "0.9.2",
        "0.9.3",
        "0.9.4"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-54691",
    "GHSA-rfr2-mq9m-x2qx"
  ],
  "details": "### Summary\n\n`datamodel-code-generator`\u0027s built-in HTTP fetcher (`http.get_body`) issues an `httpx.GET` against any URL passed to `--url` (or reached via a redirect chain) with **no allow-list, no deny-list, no IP/host validation, and `follow_redirects=True`**. Loopback addresses, RFC1918 ranges, link-local (`169.254.169.254` cloud metadata), unique-local IPv6 and any other network-accessible target are all reachable. The JSON/YAML response body is parsed as a schema and reflected into the generated `.py` source, exfiltrating the response to anyone with access to that file (commonly committed to a repository).\n\n### Details\n\nSink: `src/datamodel_code_generator/http.py`, `get_body` (lines 31\u201361, at tag `0.60.1` / commit `a321547e`):\n\n```python\ndef get_body(url, headers=None, ignore_tls=False,\n             query_parameters=None, timeout=DEFAULT_HTTP_TIMEOUT) -\u003e str:\n    httpx = _get_httpx()\n    try:\n        response = httpx.get(\n            url,\n            headers=headers,\n            verify=not ignore_tls,\n            follow_redirects=True,          # (A)\n            params=query_parameters,\n            timeout=timeout,\n        )\n    except Exception as e:\n        ...\n    if response.status_code \u003e= 400:\n        ...\n    content_type = response.headers.get(\"content-type\", \"\").lower()\n    if \"text/html\" in content_type:\n        raise SchemaFetchError(...)         # (B) \u2014 only filter\n    return response.text                    # (C) \u2192 embedded in generated.py\n```\n\n- (A) follows redirects unconditionally \u2014 a public URL \u2192 302 \u2192 internal address chain works.\n- (B) the only filter is rejecting `text/html`. Non-HTML internal endpoints (JSON APIs, cloud metadata, admin services) pass through.\n- (C) the response body becomes the schema; its `title`, `description`, `properties`, etc. land in the generated `.py` as class attributes and `Field(description=...)` strings.\n\n`get_body` is called by `parser/base.py:1326` (`_get_text_from_url`), which is reached from CLI argument `--url \u003cURL\u003e`. (The `$ref` path is a separate advisory \u2014 see GHSA-D.)\n\nOnly affects users who installed the `[http]` extra (`pip install \u0027datamodel-code-generator[http]\u0027`).\n\n### PoC\n\nA self-contained one-file PoC available here:\nhttps://gist.github.com/thegr1ffyn/18de777d6c800a3b47715425e3f3e8f5\n\n### Impact\n\n**Who is impacted.** Anyone who runs `datamodel-codegen` with a `--url` they didn\u0027t fully verify, or who runs it inside a network with reachable internal services. Realistic scenarios:\n\n1. **Trojan documentation / README.** A blog post or README example reads `datamodel-codegen --url https://schemas.example.com/user.json -o user.py`. The attacker controls `example.com`, redirects to `http://169.254.169.254/latest/meta-data/iam/security-credentials/\u003crole\u003e`, and the IAM credentials end up as a docstring in `user.py`.\n2. **Internal port scan / disclosure.** Iterating `--url http://127.0.0.1:\u003cport\u003e/health` probes localhost services; non-HTML, non-error responses confirm a service and leak its body into the generated file.\n3. **CI poisoning.** A PR adds a Makefile rule that calls `datamodel-codegen --url $(SCHEMA_URL)`; the CI runner reaches every internal service in its VPC and the response lands in PR artifacts.\n\n**Suggested fix.** Resolve the URL host, reject loopback / private / link-local / multicast / reserved IPs by default, disable redirects by default (`follow_redirects=False`), re-validate after each redirect if the user opts into following them, and add an `--allow-private-network` flag for opt-in legitimate use.\n\n### Maintainer resolution\n\nThis report was fixed together with GHSA-954p-556p-r752 by the private security PR koxudaxi/datamodel-code-generator-ghsa-rfr2-mq9m-x2qx#1, merged into the public repository as 5fdba4a09f2d7a9996a504975b7ef7d63e3715bb. Follow-up generated-file and coverage fixes were merged in koxudaxi/datamodel-code-generator#3279 and docs were synced in #3280. The patched release is 0.61.0.\n\nThe patch hardens the shared HTTP fetcher used by both direct CLI `--url` fetching and remote JSON Schema/OpenAPI `$ref` resolution:\n\n- validates HTTP(S) URLs before fetching;\n- blocks localhost, loopback, private, link-local, reserved, and other non-public network targets by default;\n- disables automatic redirect following and validates each redirect target before requesting it;\n- adds `--allow-private-network` / `allow_private_network=True` as an explicit opt-in for trusted internal schema endpoints.\n\nRemote `$ref` fetching remains controlled by `--allow-remote-refs`; non-public/internal targets additionally require `--allow-private-network`.\n\nSubmitted by: Hamza Haroon (thegr1ffyn)",
  "id": "PYSEC-2026-3564",
  "modified": "2026-08-04T13:36:16.026867Z",
  "published": "2026-08-04T11:34:44.095724Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/koxudaxi/datamodel-code-generator/security/advisories/GHSA-rfr2-mq9m-x2qx"
    },
    {
      "type": "WEB",
      "url": "https://github.com/koxudaxi/datamodel-code-generator/commit/5fdba4a09f2d7a9996a504975b7ef7d63e3715bb"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/koxudaxi/datamodel-code-generator"
    },
    {
      "type": "WEB",
      "url": "https://github.com/koxudaxi/datamodel-code-generator/releases/tag/0.61.0"
    },
    {
      "type": "PACKAGE",
      "url": "https://pypi.org/project/datamodel-code-generator"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-rfr2-mq9m-x2qx"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54691"
    }
  ],
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "datamodel-code-generator vulnerable to SSRF via --url: no host/IP validation, follows redirects"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…