From b7b666b72f74657c0da60bea0fef501ca2c62097 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Tue, 6 Oct 2026 15:22:56 -0400 Subject: [PATCH 1/4] Add optional allow-list for fetching json schemas --- core/pystac/validation/stac_validator.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/core/pystac/validation/stac_validator.py b/core/pystac/validation/stac_validator.py index 8024b0a36..71cd071c1 100644 --- a/core/pystac/validation/stac_validator.py +++ b/core/pystac/validation/stac_validator.py @@ -142,6 +142,9 @@ class JsonSchemaSTACValidator(STACValidator): the validator will retrieve the JSON schemas for validation. Defaults to an instance of :class:`~pystac.validation.schema_uri_map.DefaultSchemaUriMap` + allow_list: Optional list of base urls that are the validator can read from + when validating. Defaults to ``None`` which means that the validator + can read from anywhere. Note: This class requires the ``jsonschema`` library to be installed. @@ -149,8 +152,13 @@ class JsonSchemaSTACValidator(STACValidator): schema_uri_map: SchemaUriMap schema_cache: dict[str, dict[str, Any]] + allow_list: list[str] | None - def __init__(self, schema_uri_map: SchemaUriMap | None = None) -> None: + def __init__( + self, + schema_uri_map: SchemaUriMap | None = None, + allow_list: list[str] | None = None, + ) -> None: if not HAS_JSONSCHEMA: raise ImportError("Cannot instantiate, requires jsonschema package") @@ -159,11 +167,18 @@ def __init__(self, schema_uri_map: SchemaUriMap | None = None) -> None: else: self.schema_uri_map = DefaultSchemaUriMap() + self.allow_list = allow_list self.schema_cache = get_local_schema_cache() def _get_schema(self, schema_uri: str) -> dict[str, Any]: if schema_uri not in self.schema_cache: try: + if self.allow_list is not None and not any( + [schema_uri.startswith(base_url) for base_url in self.allow_list] + ): + raise ValueError( + f"{schema_uri=} is not in the allow-list {self.allow_list}" + ) s = json.loads(pystac.StacIO.default().read_text(schema_uri)) except Exception as error: raise GetSchemaError(schema_uri, error) from error From d327e94725c4d35d53beebd7f0893b99af3184b5 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 12 Oct 2026 13:08:26 -0400 Subject: [PATCH 2/4] Add SecureJsonSchemaSTACValidator --- core/pystac/validation/stac_validator.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/pystac/validation/stac_validator.py b/core/pystac/validation/stac_validator.py index 71cd071c1..920dfe87c 100644 --- a/core/pystac/validation/stac_validator.py +++ b/core/pystac/validation/stac_validator.py @@ -152,7 +152,7 @@ class JsonSchemaSTACValidator(STACValidator): schema_uri_map: SchemaUriMap schema_cache: dict[str, dict[str, Any]] - allow_list: list[str] | None + allow_list: list[str] | None = None def __init__( self, @@ -167,7 +167,7 @@ def __init__( else: self.schema_uri_map = DefaultSchemaUriMap() - self.allow_list = allow_list + self.allow_list = self.allow_list if allow_list is None else allow_list self.schema_cache = get_local_schema_cache() def _get_schema(self, schema_uri: str) -> dict[str, Any]: @@ -317,3 +317,10 @@ def validate_extension( self._validate_from_uri(stac_dict, stac_object_type, schema_uri, href) return schema_uri + + +class SecureJsonSchemaSTACValidator(JsonSchemaSTACValidator): + allow_list: list[str] = [ + "https://schemas.stacspec.org/", + "https://stac-extensions.github.io/", + ] From ba124984ca13e20650e6acc107b62c5a4992e3c0 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 12 Oct 2026 13:08:40 -0400 Subject: [PATCH 3/4] Add tests for SecureJsonSchemaSTACValidator --- ...ecure_validator_allows_trusted_schema.yaml | 70 +++++++++++++++++++ tests/validation/test_validate.py | 17 +++++ 2 files changed, 87 insertions(+) create mode 100644 tests/validation/cassettes/test_validate/test_secure_validator_allows_trusted_schema.yaml diff --git a/tests/validation/cassettes/test_validate/test_secure_validator_allows_trusted_schema.yaml b/tests/validation/cassettes/test_validate/test_secure_validator_allows_trusted_schema.yaml new file mode 100644 index 000000000..a595d2543 --- /dev/null +++ b/tests/validation/cassettes/test_validate/test_secure_validator_allows_trusted_schema.yaml @@ -0,0 +1,70 @@ +interactions: +- request: + body: null + headers: {} + method: GET + uri: https://stac-extensions.github.io/projection/v2.0.0/schema.json + response: + body: + string: "{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"$id\": + \"https://stac-extensions.github.io/projection/v2.0.0/schema.json\",\n \"title\": + \"Projection Extension\",\n \"description\": \"STAC Projection Extension + for STAC Items.\",\n \"$comment\": \"This schema succeeds if the proj: fields + are not used at all, please keep this in mind.\",\n \"oneOf\": [\n {\n + \ \"$comment\": \"This is the schema for STAC Items.\",\n \"allOf\": + [\n {\n \"$ref\": \"#/definitions/stac_extensions\"\n },\n + \ {\n \"type\": \"object\",\n \"required\": [\n \"type\",\n + \ \"properties\",\n \"assets\"\n ],\n \"properties\": + {\n \"type\": {\n \"const\": \"Feature\"\n },\n + \ \"properties\": {\n \"$ref\": \"#/definitions/fields\"\n + \ },\n \"assets\": {\n \"type\": \"object\",\n + \ \"additionalProperties\": {\n \"$ref\": \"#/definitions/fields\"\n + \ }\n }\n }\n }\n ]\n },\n + \ {\n \"$comment\": \"This is the schema for STAC Collections.\",\n + \ \"allOf\": [\n {\n \"type\": \"object\",\n \"required\": + [\n \"type\"\n ],\n \"properties\": {\n \"type\": + {\n \"const\": \"Collection\"\n },\n \"assets\": + {\n \"type\": \"object\",\n \"additionalProperties\": + {\n \"$ref\": \"#/definitions/fields\"\n }\n },\n + \ \"item_assets\": {\n \"type\": \"object\",\n \"additionalProperties\": + {\n \"$ref\": \"#/definitions/fields\"\n }\n }\n + \ }\n },\n {\n \"$ref\": \"#/definitions/stac_extensions\"\n + \ }\n ]\n }\n ],\n \"definitions\": {\n \"stac_extensions\": + {\n \"type\": \"object\",\n \"required\": [\n \"stac_extensions\"\n + \ ],\n \"properties\": {\n \"stac_extensions\": {\n \"type\": + \"array\",\n \"contains\": {\n \"const\": \"https://stac-extensions.github.io/projection/v2.0.0/schema.json\"\n + \ }\n }\n }\n },\n \"fields\": {\n \"type\": + \"object\",\n \"properties\": {\n \"proj:code\":{\n \"title\":\"Projection + code\",\n \"type\":[\n \"string\",\n \"null\"\n + \ ]\n },\n \"proj:wkt2\":{\n \"title\":\"Coordinate + Reference System in WKT2 format\",\n \"type\":[\n \"string\",\n + \ \"null\"\n ]\n },\n \"proj:projjson\": + {\n \"title\":\"Coordinate Reference System in PROJJSON format\",\n + \ \"oneOf\": [\n {\n \"$ref\": \"https://proj.org/schemas/v0.7/projjson.schema.json\"\n + \ },\n {\n \"type\": \"null\"\n }\n + \ ]\n },\n \"proj:geometry\":{\n \"$ref\": + \"https://geojson.org/schema/Geometry.json\"\n },\n \"proj:bbox\":{\n + \ \"title\":\"Extent\",\n \"type\":\"array\",\n \"oneOf\": + [\n {\n \"minItems\":4,\n \"maxItems\":4\n + \ },\n {\n \"minItems\":6,\n \"maxItems\":6\n + \ }\n ],\n \"items\":{\n \"type\":\"number\"\n + \ }\n },\n \"proj:centroid\":{\n \"title\":\"Centroid\",\n + \ \"type\":\"object\",\n \"required\": [\n \"lat\",\n + \ \"lon\"\n ],\n \"properties\": {\n \"lat\": + {\n \"type\": \"number\",\n \"minimum\": -90,\n + \ \"maximum\": 90\n },\n \"lon\": {\n \"type\": + \"number\",\n \"minimum\": -180,\n \"maximum\": + 180\n }\n }\n },\n \"proj:shape\":{\n \"title\":\"Shape\",\n + \ \"type\":\"array\",\n \"minItems\":2,\n \"maxItems\":2,\n + \ \"items\":{\n \"type\":\"integer\"\n }\n },\n + \ \"proj:transform\":{\n \"title\":\"Transform\",\n \"type\":\"array\",\n + \ \"oneOf\": [\n {\n \"minItems\":6,\n \"maxItems\":6\n + \ },\n {\n \"minItems\":9,\n \"maxItems\":9\n + \ }\n ],\n \"items\":{\n \"type\":\"number\"\n + \ }\n }\n },\n \"patternProperties\": {\n \"^(?!proj:)\": + {}\n },\n \"additionalProperties\": false\n }\n }\n}" + headers: {} + status: + code: 200 + message: OK +version: 1 diff --git a/tests/validation/test_validate.py b/tests/validation/test_validate.py index 2c42c6750..72100155d 100644 --- a/tests/validation/test_validate.py +++ b/tests/validation/test_validate.py @@ -15,6 +15,7 @@ from pystac.serialization.common_properties import merge_common_properties from pystac.utils import get_opt from pystac.validation import GetSchemaError, JsonSchemaSTACValidator +from pystac.validation.stac_validator import SecureJsonSchemaSTACValidator from tests.utils import TestCases from tests.utils.test_cases import ExampleInfo @@ -259,3 +260,19 @@ def test_404_schema_url(requests_mock: Mocker, item: pystac.Item) -> None: GetSchemaError, match="http://pystac-extensions.test/a-fake.schema.json" ): item.validate() + + +@pytest.mark.vcr() +def test_secure_validator_allows_trusted_schema(item: pystac.Item) -> None: + schema_uri = "https://stac-extensions.github.io/projection/v2.0.0/schema.json" + item.stac_extensions.append(schema_uri) + + item.validate(validator=SecureJsonSchemaSTACValidator()) + + +def test_secure_validator_rejects_untrusted_schema(item: pystac.Item) -> None: + schema_uri = "https://untrusted.example/schema.json" + item.stac_extensions.append(schema_uri) + + with pytest.raises(GetSchemaError, match="not in the allow-list"): + item.validate(validator=SecureJsonSchemaSTACValidator()) From 8278420dbb8cd8b72959a3156c54a3d68f69b702 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 12 Oct 2026 13:15:44 -0400 Subject: [PATCH 4/4] Add some minimal docs --- core/pystac/validation/stac_validator.py | 4 ++++ docs/api/validation/stac_validator.rst | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/core/pystac/validation/stac_validator.py b/core/pystac/validation/stac_validator.py index 920dfe87c..d9b9b7918 100644 --- a/core/pystac/validation/stac_validator.py +++ b/core/pystac/validation/stac_validator.py @@ -320,6 +320,10 @@ def validate_extension( class SecureJsonSchemaSTACValidator(JsonSchemaSTACValidator): + """Validate STAC, but only allow fetching schemas from domains that + are standard to the STAC organization. + """ + allow_list: list[str] = [ "https://schemas.stacspec.org/", "https://stac-extensions.github.io/", diff --git a/docs/api/validation/stac_validator.rst b/docs/api/validation/stac_validator.rst index 651a5c669..d27e4670d 100644 --- a/docs/api/validation/stac_validator.rst +++ b/docs/api/validation/stac_validator.rst @@ -6,6 +6,11 @@ pystac.validation.stac\_validator :undoc-members: :noindex: +.. autoclass:: pystac.validation.stac_validator.SecureJsonSchemaSTACValidator + :members: + :undoc-members: + :noindex: + .. automodule:: pystac.validation.stac_validator :members: :undoc-members: