From 90dd56152304eddd4dedb86665a7c503df5fc90a Mon Sep 17 00:00:00 2001 From: Sam Cranford Date: Wed, 24 Jun 2026 18:52:04 +0000 Subject: [PATCH 1/2] Add logic to block path traversal in static files --- ogc/edr/edr_routes.py | 9 +++++---- ogc/edr/test/test_edr_routes.py | 14 ++++++++++++++ ogc/test/test_servers.py | 22 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/ogc/edr/edr_routes.py b/ogc/edr/edr_routes.py index b03435b..e4b41f5 100644 --- a/ogc/edr/edr_routes.py +++ b/ogc/edr/edr_routes.py @@ -7,6 +7,7 @@ import pygeoapi.plugin import pygeoapi.api from typing import Tuple, Any, Dict, Generator +from werkzeug.security import safe_join from http import HTTPStatus from copy import deepcopy from pygeoapi.openapi import get_oas @@ -107,11 +108,11 @@ def static_files(self, request: pygeoapi.api.APIRequest, file_path: str) -> Tupl static_path = os.path.join(os.path.dirname(pygeoapi.__file__), "static") if "templates" in self.api.config["server"]: static_path = self.api.config["server"]["templates"].get("static", static_path) - file_path = os.path.join(static_path, file_path) - if os.path.isfile(file_path): - mime_type, _ = mimetypes.guess_type(file_path) + safe_path = safe_join(static_path, file_path) + if safe_path is not None and os.path.isfile(safe_path): + mime_type, _ = mimetypes.guess_type(safe_path) mime_type = mime_type or "application/octet-stream" - with open(file_path, "rb") as f: + with open(safe_path, "rb") as f: content = f.read() return {"Content-Type": mime_type}, HTTPStatus.OK, content else: diff --git a/ogc/edr/test/test_edr_routes.py b/ogc/edr/test/test_edr_routes.py index aa0770e..01a5dc0 100644 --- a/ogc/edr/test/test_edr_routes.py +++ b/ogc/edr/test/test_edr_routes.py @@ -44,6 +44,20 @@ def test_edr_routes_static_files_valid_path(): assert headers["Content-Type"] == "image/png" +def test_edr_routes_static_files_prevents_path_traversal(): + """Test the EDR static routes prevents path traversal.""" + request = mock_request() + edr_routes = EdrRoutes(layers=[]) + + static_path = os.path.join(os.path.dirname(__file__), "..", "static") + file_path = os.path.join(os.path.dirname(__file__), "..") + with tempfile.NamedTemporaryFile(dir=file_path) as temp_file: + relative_path = os.path.relpath(temp_file.name, static_path) + _, status, _ = edr_routes.static_files(request, relative_path) + assert os.path.exists(temp_file.name) + assert status == HTTPStatus.NOT_FOUND + + def test_edr_routes_static_files_invalid_path(): """Test the EDR static routes with an invalid static file path.""" request = mock_request() diff --git a/ogc/test/test_servers.py b/ogc/test/test_servers.py index ca8a9f3..2d65c33 100644 --- a/ogc/test/test_servers.py +++ b/ogc/test/test_servers.py @@ -1,3 +1,5 @@ +import os +import tempfile from ogc import servers from ogc import core from ogc import podpac as pogc @@ -179,6 +181,26 @@ def test_edr_render_post_returns_405(enable_edr_in_env, client): assert response.status_code == 405 +def test_edr_render_static_file_returns_200(enable_edr_in_env, client): + static_path = os.path.join(os.path.dirname(__file__), "..", "edr", "static") + file_path = static_path + with tempfile.NamedTemporaryFile(dir=file_path) as temp_file: + relative_path = os.path.relpath(temp_file.name, static_path) + response = client.get(f"/ogc/edr/static/{relative_path}") + assert os.path.exists(temp_file.name) + assert response.status_code == 200 + + +def test_edr_render_static_file_path_traversal_returns_404(enable_edr_in_env, client): + static_path = os.path.join(os.path.dirname(__file__), "..", "edr", "static") + file_path = os.path.join(os.path.dirname(__file__), "..", "edr") + with tempfile.NamedTemporaryFile(dir=file_path) as temp_file: + relative_path = os.path.relpath(temp_file.name, static_path) + response = client.get(f"/ogc/edr/static/{relative_path}") + assert os.path.exists(temp_file.name) + assert response.status_code == 404 + + def test_edr_render_query_string_too_long_returns_400(enable_edr_in_env, client): oversized = "f=json&" + "A=" + "B" * settings.MAX_QUERY_STRING_BYTES response = client.get("/ogc/edr", environ_overrides={"QUERY_STRING": oversized}) From 16c7319d8dea28879624ae2ca995a8c5fa30a0de Mon Sep 17 00:00:00 2001 From: Sam Cranford Date: Thu, 25 Jun 2026 10:32:12 -0400 Subject: [PATCH 2/2] Comment for safe_join usage --- ogc/edr/edr_routes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ogc/edr/edr_routes.py b/ogc/edr/edr_routes.py index e4b41f5..385d5d9 100644 --- a/ogc/edr/edr_routes.py +++ b/ogc/edr/edr_routes.py @@ -108,6 +108,8 @@ def static_files(self, request: pygeoapi.api.APIRequest, file_path: str) -> Tupl static_path = os.path.join(os.path.dirname(pygeoapi.__file__), "static") if "templates" in self.api.config["server"]: static_path = self.api.config["server"]["templates"].get("static", static_path) + + # Use a safe join to ensure the untrusted path is a subpath of the trusted static directory safe_path = safe_join(static_path, file_path) if safe_path is not None and os.path.isfile(safe_path): mime_type, _ = mimetypes.guess_type(safe_path)