diff --git a/README.rst b/README.rst index 60ebffe..02ba301 100644 --- a/README.rst +++ b/README.rst @@ -54,7 +54,7 @@ Usage .. code-block:: python - validate(instance, schema, cls=OAS32Validator, **kwargs) + validate(instance, schema, cls=OAS32Validator, allow_remote_references=False, **kwargs) The first argument is always the value you want to validate. The second argument is always the OpenAPI schema object. @@ -62,6 +62,10 @@ The ``cls`` keyword argument is optional and defaults to ``OAS32Validator``. Use ``cls`` when you need a specific validator version/behavior. Common forwarded keyword arguments include ``registry`` (reference context) and ``format_checker`` (format validation behavior). +By default, ``validate`` uses a local-only empty registry to avoid implicit +remote ``$ref`` retrieval. To resolve external references, pass an explicit +``registry``. Set ``allow_remote_references=True`` only if you explicitly +accept jsonschema's default remote retrieval behavior. To validate an OpenAPI schema: diff --git a/SECURITY.md b/SECURITY.md index 8ffe77b..6f3ebdc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,9 @@ If you believe you have found a security vulnerability in the repository, please **Please do not report security vulnerabilities through public GitHub issues.** -Instead, please report them directly to the repository maintainer. +Instead, please report them directly to the repository maintainers or use GitHub's private vulnerability reporting flow +(``Security Advisories``) to report security issues privately: +https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/report-a-vulnerability/privately-reporting-a-security-vulnerability Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: diff --git a/docs/references.rst b/docs/references.rst index f446bda..abec15e 100644 --- a/docs/references.rst +++ b/docs/references.rst @@ -4,6 +4,10 @@ References You can resolve JSON Schema references by passing registry. The ``validate(instance, schema, ...)`` shortcut resolves local references (``#/...``) against the provided ``schema`` mapping. +By default, the shortcut uses a local-only empty registry and does not +implicitly retrieve remote references. +If needed, ``allow_remote_references=True`` enables jsonschema's default +remote retrieval behavior. .. code-block:: python diff --git a/docs/validation.rst b/docs/validation.rst index f30c61e..7d3b00d 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -10,17 +10,24 @@ Validate .. code-block:: python - validate(instance, schema, cls=OAS32Validator, **kwargs) + validate(instance, schema, cls=OAS32Validator, allow_remote_references=False, **kwargs) The first argument is always the value you want to validate. The second argument is always the OpenAPI schema object. The ``cls`` keyword argument is optional and defaults to ``OAS32Validator``. Use ``cls`` when you need a specific validator version/behavior. +The ``allow_remote_references`` keyword argument is optional and defaults to +``False``. Common forwarded keyword arguments include: -- ``registry`` for reference resolution context +- ``registry`` for explicit external reference resolution context - ``format_checker`` to control format validation behavior +By default, ``validate`` uses a local-only empty registry to avoid implicit +remote ``$ref`` retrieval. +Set ``allow_remote_references=True`` only if you explicitly accept +jsonschema's default remote retrieval behavior. + To validate an OpenAPI schema: .. code-block:: python diff --git a/openapi_schema_validator/shortcuts.py b/openapi_schema_validator/shortcuts.py index 3ab498f..adf35b8 100644 --- a/openapi_schema_validator/shortcuts.py +++ b/openapi_schema_validator/shortcuts.py @@ -4,6 +4,7 @@ from jsonschema.exceptions import best_match from jsonschema.protocols import Validator +from referencing import Registry from openapi_schema_validator._dialects import OAS31_BASE_DIALECT_ID from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_ID @@ -16,6 +17,7 @@ def validate( schema: Mapping[str, Any], cls: type[Validator] = OAS32Validator, *args: Any, + allow_remote_references: bool = False, **kwargs: Any ) -> None: """ @@ -33,8 +35,13 @@ def validate( (``#/...``) are resolved against this mapping. cls: Validator class to use. Defaults to ``OAS32Validator``. *args: Positional arguments forwarded to ``cls`` constructor. + allow_remote_references: If ``True`` and no explicit ``registry`` is + provided, allow jsonschema's default remote reference retrieval + behavior. **kwargs: Keyword arguments forwarded to ``cls`` constructor - (for example ``registry`` and ``format_checker``). + (for example ``registry`` and ``format_checker``). If omitted, + a local-only empty ``Registry`` is used to avoid implicit remote + reference retrieval. Raises: jsonschema.exceptions.SchemaError: If ``schema`` is invalid. @@ -54,7 +61,11 @@ def validate( else: cls.check_schema(schema_dict) - validator = cls(schema_dict, *args, **kwargs) + validator_kwargs = kwargs.copy() + if not allow_remote_references: + validator_kwargs.setdefault("registry", Registry()) + + validator = cls(schema_dict, *args, **validator_kwargs) error = best_match( validator.evolve(schema=schema_dict).iter_errors(instance) ) diff --git a/tests/unit/test_shortcut.py b/tests/unit/test_shortcut.py index e391d75..f171f6f 100644 --- a/tests/unit/test_shortcut.py +++ b/tests/unit/test_shortcut.py @@ -2,6 +2,8 @@ from unittest.mock import patch import pytest +from referencing import Registry +from referencing import Resource from openapi_schema_validator import OAS32Validator from openapi_schema_validator import validate @@ -56,3 +58,63 @@ def test_oas32_validate_does_not_fetch_remote_metaschemas(schema): validate({"email": "foo@bar.com"}, schema, cls=OAS32Validator) urlopen.assert_not_called() + + +def test_validate_blocks_implicit_remote_http_references_by_default(): + schema = {"$ref": "http://example.com/remote-schema.json"} + + with patch("urllib.request.urlopen") as urlopen: + with pytest.raises(Exception, match="Unresolvable"): + validate({}, schema) + + urlopen.assert_not_called() + + +def test_validate_blocks_implicit_file_references_by_default(): + schema = {"$ref": "file:///etc/hosts"} + + with patch("urllib.request.urlopen") as urlopen: + with pytest.raises(Exception, match="Unresolvable"): + validate({}, schema) + + urlopen.assert_not_called() + + +def test_validate_local_references_still_work_by_default(): + schema = {"$defs": {"Value": {"type": "integer"}}, "$ref": "#/$defs/Value"} + + with patch("urllib.request.urlopen") as urlopen: + result = validate(1, schema) + + assert result is None + urlopen.assert_not_called() + + +def test_validate_honors_explicit_registry(): + schema = { + "type": "object", + "properties": {"name": {"$ref": "urn:name-schema"}}, + } + name_schema = Resource.from_contents( + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + } + ) + registry = Registry().with_resources( + [("urn:name-schema", name_schema)], + ) + + result = validate({"name": "John"}, schema, registry=registry) + + assert result is None + + +def test_validate_can_allow_implicit_remote_references(): + schema = {"$ref": "http://example.com/remote-schema.json"} + + with patch("urllib.request.urlopen") as urlopen: + with pytest.raises(Exception): + validate({}, schema, allow_remote_references=True) + + assert urlopen.called