Root cause
Path & link handling
Add --allow-remote-refs to disable HTTP fetching of $ref by default (#3051)
AI-assisted change: Claude
How AI contributed: Flawed AI-written codePublished Jul 28, 2026 in
koxudaxi/datamodel-code-generator
Summary 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): The remote-ref gate then explicitly exempts file://, so --no-allow-remote-refs never applies to it (parser/jsonschema.py, _get_ref_body): Both local-file branches read the target with no containment check. The file:// branch reads any absolute path: and the plain relative branch lets ../ escape the base directory: 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: 1. 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. 2. 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 — 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): 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 1. Do not exempt file:// from the allow_remote_refs gate, treat it as an external scheme. 2. 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. 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)
The JSON-Schema parser reads local $ref targets without checking that they stay under the input base directory: the file:// branch hands the URL path straight to load_data_from_path, and the relative branch joins base_path with the reference so a ../ segment escapes the tree. The remote-reference gate also exempts file:// references, so --no-allow-remote-refs never blocks them.
Root cause
Add --allow-remote-refs to disable HTTP fetching of $ref by default (#3051)
AI-assisted change: Claude
Fix
Checks that local JSON-Schema references resolve inside the input base directory and blocks out-of-tree file:// and relative paths when remote references are disabled, unless the caller explicitly allows them.
Case facts
diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.pyindex 2665abae..116fc274 100644--- a/src/datamodel_code_generator/parser/jsonschema.py+++ b/src/datamodel_code_generator/parser/jsonschema.py@@ -1964,8 +1964,21 @@ class JsonSchemaParser(Parser): return self._get_ref_body_from_remote(resolved_ref) def _get_ref_body_from_url(self, ref: str) -> dict[str, YamlValue]:- """Get reference body from a URL."""- # URL Reference: $ref: 'http://path/to/your/resource' Uses the whole document located on the different server.+ """Get reference body from a URL (HTTP, HTTPS, or file scheme)."""+ if ref.startswith("file://"):+ from urllib.parse import urlparse # noqa: PLC0415+ from urllib.request import url2pathname # noqa: PLC0415++ parsed = urlparse(ref)+ # url2pathname handles percent-decoding and Windows drive letters+ path = url2pathname(parsed.path)+ # Handle UNC paths (file://server/share/path)+ if parsed.netloc:+ path = f"//{parsed.netloc}{path}"+ file_path = Path(path)+ return self.remote_object_cache.get_or_put(+ ref, default_factory=lambda _: load_yaml_dict_from_path(file_path, self.encoding)+ ) return self.remote_object_cache.get_or_put( ref, default_factory=lambda key: load_yaml_dict(self._get_text_from_url(key)) )diff --git a/src/datamodel_code_generator/http.py b/src/datamodel_code_generator/http.pyindex abb75c5d..015a8780 100644--- a/src/datamodel_code_generator/http.py+++ b/src/datamodel_code_generator/http.py@@ -1,21 +1,26 @@ """HTTP utilities for fetching remote schema files. Provides functions to fetch schema content from URLs and join URL references.-Requires the 'http' extra: `pip install 'datamodel-code-generator[http]'`.+HTTP(S) URLs require the 'http' extra: `pip install 'datamodel-code-generator[http]'`.+file:// URLs are handled without additional dependencies. """ from __future__ import annotations -from typing import TYPE_CHECKING+from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from collections.abc import Sequence -try:- import httpx-except ImportError as exc: # pragma: no cover- msg = "Please run `$pip install 'datamodel-code-generator[http]`' to resolve URL Reference"- raise Exception(msg) from exc # noqa: TRY002++def _get_httpx() -> Any:+ """Lazily import httpx, raising a helpful error if not installed."""+ try:+ import httpx # noqa: PLC0415+ except ImportError as exc: # pragma: no cover+ msg = "Please run `$pip install 'datamodel-code-generator[http]`' to resolve HTTP(S) URL references"+ raise Exception(msg) from exc # noqa: TRY002+ return httpx def get_body(@@ -25,6 +30,7 @@ def get_body( query_parameters: Sequence[tuple[str, str]] | None = None, ) -> str: """Fetch content from a URL with optional headers and query parameters."""+ httpx = _get_httpx() return httpx.get( url, headers=headers,@@ -35,6 +41,51 @@ def get_body( ).text -def join_url(url: str, ref: str = ".") -> str:+def join_url(url: str, ref: str = ".") -> str: # noqa: PLR0912 """Join a base URL with a relative reference."""+ if url.startswith("file://"):+ from urllib.parse import urlparse # noqa: PLC0415++ parsed = urlparse(url)++ if ref.startswith("file://"):+ return ref++ ref_path, *frag = ref.split("#", 1)++ # Fragment-only ref: keep the original path+ if not ref_path:+ joined = url.split("#", maxsplit=1)[0]+ if frag:+ joined += f"#{frag[0]}"+ return joined++ if ref_path.startswith("/"):+ joined_path = ref_path+ else:+ base_segments = parsed.path.lstrip("/").split("/")+ if base_segments and not base_segments[0]:+ base_segments = []+ if base_segments:+ base_segments = base_segments[:-1]++ min_depth = 1 if parsed.netloc else 0+ for segment in ref_path.split("/"):+ if segment in {"", "."}:+ continue+ if segment == "..":+ if len(base_segments) > min_depth:+ base_segments.pop()+ continue+ base_segments.append(segment)++ joined_path = "/" + "/".join(base_segments)+ if ref_path.endswith("/"):+ joined_path += "/"++ joined = f"file://{parsed.netloc}{joined_path}"+ if frag:+ joined += f"#{frag[0]}"+ return joined+ httpx = _get_httpx() return str(httpx.URL(url).join(ref))diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.pyindex 065b4896..71a78c96 100644--- a/src/datamodel_code_generator/parser/jsonschema.py+++ b/src/datamodel_code_generator/parser/jsonschema.py@@ -4815 +4815,3 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]):- if not resolved_ref.startswith("file://") and self.http_local_ref_path is None:+ url_scheme = urlparse(resolved_ref).scheme+ uses_local_http_path = url_scheme in {"http", "https"} and self.http_local_ref_path is not None+ if not uses_local_http_path:@@ -4819 +4821 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]):- "Reason: --no-allow-remote-refs was set, so HTTP(S) $ref targets are not fetched.\n"+ "Reason: --no-allow-remote-refs was set, so external $ref targets are not fetched.\n"@@ -4825 +4827 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]):- if self.allow_remote_refs is None:+ if self.allow_remote_refs is None and url_scheme in {"http", "https"}:@@ -4837,0 +4840,26 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]):+ def _resolve_local_ref_path(self, path: Path, ref: str) -> Path:+ base_path = self.base_path.resolve()+ resolved_path = path.resolve()+ if resolved_path.is_relative_to(base_path) or self.allow_remote_refs is True:+ return resolved_path++ details = (+ f"Reference: {ref}. Reason: the resolved file is outside the input base path. "+ f"Base path: {base_path}. Resolved path: {resolved_path}. "+ "Move trusted referenced schemas under the input directory, pass --allow-remote-refs to allow this "+ "external local file reference without a warning, or pass --no-allow-remote-refs to block it."+ )+ if self.allow_remote_refs is None:+ warn_deprecated("behavior.remote-ref-default", details=details, stacklevel=3)+ return resolved_path++ msg = (+ f"Blocked unsafe local $ref: {ref}\n"+ "Reason: --no-allow-remote-refs was set and the resolved file is outside the input base path.\n"+ f"Base path: {base_path}\n"+ f"Resolved path: {resolved_path}\n"+ "Move trusted referenced schemas under the input directory, or pass --allow-remote-refs only when the "+ "schema and referenced files are trusted."+ )+ raise Error(msg)+@@ -4881 +4909 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]):- file_path = Path(path)+ file_path = self._resolve_local_ref_path(Path(path), ref)@@ -4883 +4911 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]):- ref, default_factory=lambda _: load_data_from_path(file_path, self.encoding)+ str(file_path), default_factory=lambda _: load_data_from_path(file_path, self.encoding)@@ -4893 +4921 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]):- full_path = self.base_path / resolved_ref+ full_path = self._resolve_local_ref_path(self.base_path / resolved_ref, resolved_ref)AI-assisted change bd5533c9dde07f5254f22a2aa229edc413f2a396ed0a4ec3afde37e51cfef2da · Fix 75a1ef0b2ff45227ce66de98b564d55ff203e7e0bf131f4c59fb78829cd78bad