PYSEC-2026-3558
Vulnerability from pysec - Published: 2026-08-04 11:34 - Updated: 2026-08-04 13:36Summary
datamodel-code-generator resolves JSON-Schema $ref targets that point at the local filesystem without restricting them to the input/base directory and without honoring the remote-reference security control. In the default configuration, an attacker who controls an input schema (a "paste your OpenAPI/JSON-Schema" service, a CI job that generates models from a submitted spec, or any multi-tenant codegen platform) can read any file the process user can read and map the host filesystem. This works via either a file:// absolute URI or a ../-escaped relative reference, and it succeeds even when --no-allow-remote-refs is set.
This is an unauthenticated path-traversal / information-disclosure issue (CWE-22 / CWE-200) plus a bypass of a documented security control.
Details
is_url() classifies file:// as a URL (reference.py:1249):
def is_url(ref: str) -> bool:
return ref.startswith(("https://", "http://", "file://"))
The remote-ref gate then explicitly exempts file://, so --no-allow-remote-refs never applies to it (parser/jsonschema.py, _get_ref_body):
if is_url(resolved_ref):
if not resolved_ref.startswith("file://") and self.http_local_ref_path is None:
if self.allow_remote_refs is False:
raise Error(...) # <-- skipped for file://
...
return self._get_ref_body_from_url(resolved_ref)
return self._get_ref_body_from_remote(resolved_ref)
Both local-file branches read the target with no containment check. The file:// branch reads any absolute path:
# _get_ref_body_from_url
if ref.startswith("file://"):
path = url2pathname(urlparse(ref).path) # absolute path, anywhere on disk
return self.remote_object_cache.get_or_put(
ref, default_factory=lambda _: load_data_from_path(Path(path), self.encoding))
and the plain relative branch lets ../ escape the base directory:
# _get_ref_body_from_remote
full_path = self.base_path / resolved_ref # no is_relative_to(base_path) check
return self.remote_object_cache.get_or_put(
str(full_path), default_factory=lambda _: load_data_from_path(full_path, self.encoding))
For contrast, the HTTP-local-ref branch (_get_ref_body_from_local_http_path) does enforce is_relative_to(base_path); these two filesystem branches do not. (This is distinct from the previously reported HTTP $ref/--url SSRF hardened in http.py these code paths never enter http.py.)
The fetched file is read and parsed, yielding two impacts:
-
Arbitrary file read + filesystem oracle (any file). The process opens any readable path, and the three distinguishable outcomes leak filesystem structure:
Expected dict, got str/list= the file exists and was read into the process;FileNotFoundError: '<abs path>'= missing;PermissionError: '<abs path>'= exists but unreadable. An attacker uses this to probe arbitrary paths and recover the operator's absolute filesystem layout. -
Verbatim secret disclosure into the generated code. When the referenced node is JSON-Schema-shaped, values in
const/default/enum/descriptionpositions are emitted verbatim into the generated Python that is returned to the attacker — i.e. exactly the internal/private schema and config files a codegen host stores (specs with example/default tokens, default credentials, internal hostnames, service-account JSON, k8s secret manifests).
Scope note (to avoid overstating): the raw bytes of unstructured files such as /etc/passwd or a PEM id_rsa are read into the process but are not echoed verbatim into the output, because they parse to a single scalar and are rejected as "not a schema". For those files the impact is the read itself plus the existence/permission/absolute-path oracle; verbatim content disclosure applies to schema-shaped nodes.
PoC
Self-contained reproducer: https://gist.github.com/thegr1ffyn/2a87e81f985883acc30d0118c52da4d3 / (poc.py creates everything in a temp dir, runs the generator, asserts read + leak + gate bypass, and cleans up).
Minimal manual reproduction of the arbitrary read (any path works):
printf '{"type":"object","properties":{"x":{"$ref":"file:///etc/passwd"}}}' > probe.json
datamodel-codegen --input probe.json --input-file-type jsonschema --output o.py
# -> "TypeError: Expected dict, got str" == /etc/passwd was opened and fully parsed.
# Swap the $ref for a missing/unreadable path to see the existence/permission oracle.
Impact
Arbitrary local file read / path traversal (CWE-22) → information disclosure (CWE-200), plus bypass of --no-allow-remote-refs. Any application, CI pipeline, or multi-tenant service that runs datamodel-code-generator on an untrusted schema and exposes (returns, logs, commits, renders) the generated code is affected. The attacker can: open and read any file the process user can read (demonstrated: /etc/passwd, a PEM id_rsa, paths under /root/); map the host filesystem and recover absolute paths; and exfiltrate secret values verbatim from any schema-shaped node. Attacker controls only the input schema; no authentication or special privileges required.
Suggested remediation
- Do not exempt
file://from theallow_remote_refsgate, treat it as an external scheme. - In both
_get_ref_body_from_url(thefile://case) and_get_ref_body_from_remote, reject targets that are notresolved.is_relative_to(self.base_path), mirroring the existing check in_get_ref_body_from_local_http_path.
Maintainer status
Confirmed by maintainer review and regression tests. The private fix PR was merged and released in 0.62.0: https://github.com/koxudaxi/datamodel-code-generator-ghsa-8359-h9fx-j6v9/pull/1
Fix summary: apply the remote-ref gate to file:// refs and local JSON Schema refs outside the input base path. In 0.62.0, --no-allow-remote-refs blocks these references; the default compatibility mode emits a FutureWarning and users who intentionally rely on trusted external local refs can pass --allow-remote-refs.
Release status: fixed in 0.62.0 for the documented --no-allow-remote-refs bypass, with a compatibility warning for the default behavior.
Validation: uv run --group test --extra http pytest tests/main/jsonschema/test_main_jsonschema.py passed locally; uv run --group fix ruff check src/datamodel_code_generator/parser/jsonschema.py tests/main/jsonschema/test_main_jsonschema.py passed.
Submitted by: Hamza Haroon (thegr1ffyn)
| 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"
},
{
"fixed": "0.62.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.0.1",
"0.0.2",
"0.0.3",
"0.0.4",
"0.0.5",
"0.0.6",
"0.1.0",
"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.2.0",
"0.2.1",
"0.2.10",
"0.2.11",
"0.2.12",
"0.2.13",
"0.2.14",
"0.2.15",
"0.2.16",
"0.2.2",
"0.2.3",
"0.2.4",
"0.2.5",
"0.2.6",
"0.2.7",
"0.2.8",
"0.2.9",
"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.3.0",
"0.3.1",
"0.3.2",
"0.3.3",
"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.4.0",
"0.4.1",
"0.4.10",
"0.4.11",
"0.4.2",
"0.4.3",
"0.4.4",
"0.4.5",
"0.4.6",
"0.4.7",
"0.4.8",
"0.4.9",
"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.5.0",
"0.5.1",
"0.5.10",
"0.5.11",
"0.5.12",
"0.5.13",
"0.5.14",
"0.5.15",
"0.5.16",
"0.5.17",
"0.5.18",
"0.5.19",
"0.5.2",
"0.5.20",
"0.5.21",
"0.5.22",
"0.5.23",
"0.5.24",
"0.5.25",
"0.5.26",
"0.5.27",
"0.5.28",
"0.5.29",
"0.5.3",
"0.5.30",
"0.5.31",
"0.5.32",
"0.5.33",
"0.5.34",
"0.5.35",
"0.5.36",
"0.5.37",
"0.5.38",
"0.5.39",
"0.5.4",
"0.5.5",
"0.5.6",
"0.5.7",
"0.5.8",
"0.5.9",
"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.6.0",
"0.6.1",
"0.6.10",
"0.6.11",
"0.6.12",
"0.6.13",
"0.6.14",
"0.6.15",
"0.6.16",
"0.6.17",
"0.6.18",
"0.6.19",
"0.6.2",
"0.6.20",
"0.6.21",
"0.6.22",
"0.6.23",
"0.6.24",
"0.6.25",
"0.6.26",
"0.6.3",
"0.6.4",
"0.6.5",
"0.6.6",
"0.6.7",
"0.6.8",
"0.6.9",
"0.60.0",
"0.60.1",
"0.60.2",
"0.61.0",
"0.7.0",
"0.7.1",
"0.7.2",
"0.7.3",
"0.8.0",
"0.8.1",
"0.8.2",
"0.8.3",
"0.9.0",
"0.9.1",
"0.9.2",
"0.9.3",
"0.9.4"
]
}
],
"aliases": [
"CVE-2026-55389",
"GHSA-8359-h9fx-j6v9"
],
"details": "### Summary\n\n`datamodel-code-generator` resolves JSON-Schema `$ref` targets that point at the local filesystem without restricting them to the input/base directory and without honoring the remote-reference security control. In the default configuration, an attacker who controls an input schema (a \"paste your OpenAPI/JSON-Schema\" service, a CI job that generates models from a submitted spec, or any multi-tenant codegen platform) can read any file the process user can read and map the host filesystem. This works via either a `file://` absolute URI or a `../`-escaped relative reference, and it succeeds even when `--no-allow-remote-refs` is set.\n\nThis is an unauthenticated path-traversal / information-disclosure issue (CWE-22 / CWE-200) plus a bypass of a documented security control.\n\n### Details\n\n`is_url()` classifies `file://` as a URL (`reference.py:1249`):\n\n```python\ndef is_url(ref: str) -\u003e bool:\n return ref.startswith((\"https://\", \"http://\", \"file://\"))\n```\n\nThe remote-ref gate then explicitly exempts `file://`, so `--no-allow-remote-refs` never applies to it (`parser/jsonschema.py`, `_get_ref_body`):\n\n```python\nif is_url(resolved_ref):\n if not resolved_ref.startswith(\"file://\") and self.http_local_ref_path is None:\n if self.allow_remote_refs is False:\n raise Error(...) # \u003c-- skipped for file://\n ...\n return self._get_ref_body_from_url(resolved_ref)\nreturn self._get_ref_body_from_remote(resolved_ref)\n```\n\nBoth local-file branches read the target with **no containment check**. The `file://` branch reads any absolute path:\n\n```python\n# _get_ref_body_from_url\nif ref.startswith(\"file://\"):\n path = url2pathname(urlparse(ref).path) # absolute path, anywhere on disk\n return self.remote_object_cache.get_or_put(\n ref, default_factory=lambda _: load_data_from_path(Path(path), self.encoding))\n```\n\nand the plain relative branch lets `../` escape the base directory:\n\n```python\n# _get_ref_body_from_remote\nfull_path = self.base_path / resolved_ref # no is_relative_to(base_path) check\nreturn self.remote_object_cache.get_or_put(\n str(full_path), default_factory=lambda _: load_data_from_path(full_path, self.encoding))\n```\n\nFor contrast, the HTTP-local-ref branch (`_get_ref_body_from_local_http_path`) *does* enforce `is_relative_to(base_path)`; these two filesystem branches do not. (This is distinct from the previously reported HTTP `$ref`/`--url` SSRF hardened in `http.py` these code paths never enter `http.py`.)\n\nThe fetched file is read and parsed, yielding two impacts:\n\n1. **Arbitrary file read + filesystem oracle (any file).** The process opens any readable path, and the three distinguishable outcomes leak filesystem structure: `Expected dict, got str/list` = the file exists and was read into the process; `FileNotFoundError: \u0027\u003cabs path\u003e\u0027` = missing; `PermissionError: \u0027\u003cabs path\u003e\u0027` = exists but unreadable. An attacker uses this to probe arbitrary paths and recover the operator\u0027s absolute filesystem layout.\n\n2. **Verbatim secret disclosure into the generated code.** When the referenced node is JSON-Schema-shaped, values in `const`/`default`/`enum`/`description` positions are emitted verbatim into the generated Python that is returned to the attacker \u2014 i.e. exactly the internal/private schema and config files a codegen host stores (specs with example/default tokens, default credentials, internal hostnames, service-account JSON, k8s secret manifests).\n\nScope note (to avoid overstating): the *raw bytes* of unstructured files such as `/etc/passwd` or a PEM `id_rsa` are read into the process but are not echoed verbatim into the output, because they parse to a single scalar and are rejected as \"not a schema\". For those files the impact is the read itself plus the existence/permission/absolute-path oracle; verbatim content disclosure applies to schema-shaped nodes.\n\n### PoC\n\nSelf-contained reproducer: https://gist.github.com/thegr1ffyn/2a87e81f985883acc30d0118c52da4d3 / (`poc.py` creates everything in a temp dir, runs the generator, asserts read + leak + gate bypass, and cleans up). \nMinimal manual reproduction of the arbitrary read (any path works):\n\n```bash\nprintf \u0027{\"type\":\"object\",\"properties\":{\"x\":{\"$ref\":\"file:///etc/passwd\"}}}\u0027 \u003e probe.json\ndatamodel-codegen --input probe.json --input-file-type jsonschema --output o.py\n# -\u003e \"TypeError: Expected dict, got str\" == /etc/passwd was opened and fully parsed.\n# Swap the $ref for a missing/unreadable path to see the existence/permission oracle.\n```\n\n### Impact\n\nArbitrary local file read / path traversal (CWE-22) \u2192 information disclosure (CWE-200), plus bypass of `--no-allow-remote-refs`. Any application, CI pipeline, or multi-tenant service that runs `datamodel-code-generator` on an untrusted schema and exposes (returns, logs, commits, renders) the generated code is affected. The attacker can: open and read any file the process user can read (demonstrated: `/etc/passwd`, a PEM `id_rsa`, paths under `/root/`); map the host filesystem and recover absolute paths; and exfiltrate secret values verbatim from any schema-shaped node. Attacker controls only the input schema; no authentication or special privileges required.\n\n### Suggested remediation\n\n1. Do not exempt `file://` from the `allow_remote_refs` gate, treat it as an external scheme.\n2. In both `_get_ref_body_from_url` (the `file://` case) and `_get_ref_body_from_remote`, reject targets that are not `resolved.is_relative_to(self.base_path)`, mirroring the existing check in `_get_ref_body_from_local_http_path`.\n\n### Maintainer status\n\nConfirmed by maintainer review and regression tests. The private fix PR was merged and released in `0.62.0`: https://github.com/koxudaxi/datamodel-code-generator-ghsa-8359-h9fx-j6v9/pull/1\n\nFix summary: apply the remote-ref gate to `file://` refs and local JSON Schema refs outside the input base path. In `0.62.0`, `--no-allow-remote-refs` blocks these references; the default compatibility mode emits a `FutureWarning` and users who intentionally rely on trusted external local refs can pass `--allow-remote-refs`.\n\nRelease status: fixed in `0.62.0` for the documented `--no-allow-remote-refs` bypass, with a compatibility warning for the default behavior.\n\nValidation: `uv run --group test --extra http pytest tests/main/jsonschema/test_main_jsonschema.py` passed locally; `uv run --group fix ruff check src/datamodel_code_generator/parser/jsonschema.py tests/main/jsonschema/test_main_jsonschema.py` passed.\n\nSubmitted by: Hamza Haroon (thegr1ffyn)",
"id": "PYSEC-2026-3558",
"modified": "2026-08-04T13:36:15.378101Z",
"published": "2026-08-04T11:34:44.697205Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/koxudaxi/datamodel-code-generator/security/advisories/GHSA-8359-h9fx-j6v9"
},
{
"type": "WEB",
"url": "https://github.com/koxudaxi/datamodel-code-generator/commit/2ff4a72b4550a2b2069754c5b075b1655067e5fb"
},
{
"type": "PACKAGE",
"url": "https://github.com/koxudaxi/datamodel-code-generator"
},
{
"type": "WEB",
"url": "https://github.com/koxudaxi/datamodel-code-generator/releases/tag/0.62.0"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/datamodel-code-generator"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-8359-h9fx-j6v9"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-55389"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "datamodel-code-generator vulnerable to arbitrary local file read via JSON-Schema `$ref` (`file://` and `../` traversal), bypassing `--no-allow-remote-refs`"
}
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.