Root cause
AI change
Add --allow-remote-refs to disable HTTP fetching of $ref by default (#3051)
AI candidate: Claude Opus 4.6 (1M context)
Loading…
How AI contributed
Flawed AI-written codedatamodel-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 work...
Root cause
Add --allow-remote-refs to disable HTTP fetching of $ref by default (#3051)
AI candidate: Claude Opus 4.6 (1M context)
Fix
Merge commit from fork
Fix by Koudai Aono · no AI marker found
Code comparison
--- a/src/datamodel_code_generator/parser/jsonschema.py+++ b/src/datamodel_code_generator/parser/jsonschema.py@@ -4812,17 +4812,19 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]): def _get_ref_body(self, resolved_ref: str) -> dict[str, YamlValue]: """Get the body of a reference from URL or remote file.""" if is_url(resolved_ref):- 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: if self.allow_remote_refs is False: msg = ( f"Fetching remote $ref is disabled: {resolved_ref}\n"- "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" "If this schema and all of its remote references are trusted, pass --allow-remote-refs. " "If a trusted remote reference points to an internal schema registry, also pass " "--allow-private-network." ) raise Error(msg)- if self.allow_remote_refs is None:+ if self.allow_remote_refs is None and url_scheme in {"http", "https"}: warn_deprecated( "behavior.remote-ref-default", details=(@@ -4835,6 +4837,32 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]): return self._get_ref_body_from_url(resolved_ref) return self._get_ref_body_from_remote(resolved_ref) + 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)+ def _get_ref_body_from_local_http_path(self, ref: str) -> dict[str, YamlValue]: assert self.http_local_ref_path is not None parsed = urlparse(ref)@@ -4878,9 +4906,9 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]): # Handle UNC paths (file://server/share/path) if parsed.netloc: path = f"//{parsed.netloc}{path}"- file_path = Path(path)+ file_path = self._resolve_local_ref_path(Path(path), ref) return self.remote_object_cache.get_or_put(- 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) ) if self.http_local_ref_path is not None and urlparse(ref).scheme in {"http", "https"}: return self._get_ref_body_from_local_http_path(ref)@@ -4890,7 +4918,7 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]): def _get_ref_body_from_remote(self, resolved_ref: str) -> dict[str, YamlValue]: """Get reference body from a remote file path."""- full_path = self.base_path / resolved_ref+ full_path = self._resolve_local_ref_path(self.base_path / resolved_ref, resolved_ref) try: return self.remote_object_cache.get_or_put(--- a/tests/main/jsonschema/test_main_jsonschema.py+++ b/tests/main/jsonschema/test_main_jsonschema.py@@ -1150,10 +1150,15 @@ def test_main_local_directory_path_absolute_root_id_refs_rejects_parent_traversa input_path=JSON_SCHEMA_DATA_PATH / "path_absolute_root_id_refs_parent_traversal" / "schema-root", output_path=output_dir, input_file_type="jsonschema",- extra_args=["--output-model-type", "pydantic_v2.BaseModel", "--disable-timestamp"],+ extra_args=[+ "--output-model-type",+ "pydantic_v2.BaseModel",+ "--disable-timestamp",+ "--no-allow-remote-refs",+ ], expected_exit=Exit.ERROR, capsys=capsys,- expected_stderr_contains="$ref file not found",+ expected_stderr_contains="Blocked unsafe local $ref", ) @@ -3311,6 +3316,120 @@ def test_main_jsonschema_default_factory_rejects_unsafe_value( ) [email protected]("ref_template", ["../secret/leak.json", "{file_uri}"])+def test_main_jsonschema_warns_local_ref_outside_base_path(+ ref_template: str,+ output_file: Path,+) -> None:+ """Keep local refs outside the input base path compatible, but warn."""+ project_dir = output_file.parent / "project"+ secret_dir = output_file.parent / "secret"+ project_dir.mkdir()+ secret_dir.mkdir()+ secret_schema = secret_dir / "leak.json"+ secret_schema.write_text(+ json.dumps({+ "type": "object",+ "properties": {+ "token": {+ "type": "string",+ "default": "SECRET_TOKEN",+ }+ },+ }),+ encoding="utf-8",+ )+ ref = ref_template.format(file_uri=secret_schema.resolve().as_uri())+ input_file = project_dir / "schema.json"+ input_file.write_text(+ json.dumps({+ "type": "object",+ "properties": {+ "payload": {+ "$ref": ref,+ }+ },+ }),+ encoding="utf-8",+ )++ with pytest.warns(FutureWarning, match=r"outside the input base path"):+ run_main_and_assert(+ input_path=input_file,+ output_path=output_file,+ input_file_type="jsonschema",+ )+++def test_main_jsonschema_no_allow_remote_refs_blocks_local_ref_outside_base_path(+ output_file: Path,+ capsys: pytest.CaptureFixture[str],+) -> None:+ """Reject local JSON Schema refs outside the input base path when explicitly disabled."""+ project_dir = output_file.parent / "project"+ secret_dir = output_file.parent / "secret"+ project_dir.mkdir()+ secret_dir.mkdir()+ (secret_dir / "leak.json").write_text(json.dumps({"type": "object"}), encoding="utf-8")+ input_file = project_dir / "schema.json"+ input_file.write_text(+ json.dumps({+ "type": "object",+ "properties": {+ "payload": {+ "$ref": "../secret/leak.json",+ }+ },+ }),+ encoding="utf-8",+ )++ run_main_and_assert(+ input_path=input_file,+ output_path=output_file,+ input_file_type="jsonschema",+ expected_exit=Exit.ERROR,+ output_should_not_exist=True,+ capsys=capsys,+ expected_stderr_contains="Blocked unsafe local $ref",+ extra_args=["--no-allow-remote-refs"],+ )+++def test_main_jsonschema_no_allow_remote_refs_blocks_file_url(+ output_file: Path,+ capsys: pytest.CaptureFixture[str],+) -> None:+ """Apply --no-allow-remote-refs to file:// refs as external refs."""+ project_dir = output_file.parent / "project"+ project_dir.mkdir()+ referenced_schema = project_dir / "referenced.json"+ referenced_schema.write_text(json.dumps({"type": "object"}), encoding="utf-8")+ input_file = project_dir / "schema.json"+ input_file.write_text(+ json.dumps({+ "type": "object",+ "properties": {+ "payload": {+ "$ref": referenced_schema.resolve().as_uri(),--- a/tests/main/openapi/test_main_openapi.py+++ b/tests/main/openapi/test_main_openapi.py@@ -2173,11 +2173,12 @@ def test_main_openapi_nullable_use_union_operator(output_file: Path) -> None: def test_external_relative_ref(tmp_path: Path) -> None: """Test OpenAPI generation with external relative references."""- run_main_and_assert(- input_path=OPEN_API_DATA_PATH / "external_relative_ref" / "model_b",- output_path=tmp_path,- expected_directory=EXPECTED_OPENAPI_PATH / "external_relative_ref",- )+ with pytest.warns(FutureWarning, match=r"outside the input base path"):+ run_main_and_assert(+ input_path=OPEN_API_DATA_PATH / "external_relative_ref" / "model_b",+ output_path=tmp_path,+ expected_directory=EXPECTED_OPENAPI_PATH / "external_relative_ref",+ ) def test_paths_external_ref(output_file: Path) -> None:@@ -2919,7 +2920,10 @@ def test_main_dataclass_base_class(output_file: Path) -> None: def test_main_openapi_reference_same_hierarchy_directory(tmp_path: Path) -> None: """Test OpenAPI generation with reference in same hierarchy directory.""" output_file: Path = tmp_path / "output.py"- with chdir(OPEN_API_DATA_PATH / "reference_same_hierarchy_directory"):+ with (+ chdir(OPEN_API_DATA_PATH / "reference_same_hierarchy_directory"),+ pytest.warns(FutureWarning, match=r"outside the input base path"),+ ): run_main_and_assert( input_path=Path("./public/entities.yaml"), output_path=output_file,Candidate e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 · Fix 3a7ea6f822fcffb33eda6d88203f3c4f4799a85648e967e8ea376ec5fc5e3037
Releases
Advisory references