From 0bf8ede0a186a1d400db62bffe92affe75c6843e Mon Sep 17 00:00:00 2001 From: bayotte Date: Fri, 7 Aug 2026 10:06:07 -0400 Subject: [PATCH 1/3] updating allowed chars --- ogc/servers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ogc/servers.py b/ogc/servers.py index 71a75d8..0d3835e 100755 --- a/ogc/servers.py +++ b/ogc/servers.py @@ -302,8 +302,8 @@ def ogc_render(self, ogc_idx): # so we allow braces, brackets, and quotes. # Allowed chars are: # -, A through Z, a through z, 0 through 9, - # and the characters + . , _ / : * { } ( ) [ ] " - allowed_chars = r'-A-Za-z0-9+.,_/:*\{\}\(\)\[\]"' + # and the characters + . , _ / : * { } ( ) [ ] " % ^ + allowed_chars = r'-A-Za-z0-9+.,_/:*\{\}\(\)\[\]"%^' match_one_unallowed_char = "[^%s]" % allowed_chars args = { # WCS standard says argument keys can come in with any @@ -389,7 +389,7 @@ def wrapper(*args, **kwargs) -> Response: # Allowed chars are: # -, A through Z, a through z, 0 through 9, spaces # and the characters + . , _ / : * { } ( ) [ ] " - allowed_chars = r'-A-Za-z0-9 +.,_/:*\{\}\(\)\[\]"' + allowed_chars = r'-A-Za-z0-9 +.,_/:*\{\}\(\)\[\]"%^' match_one_unallowed_char = "[^%s]" % allowed_chars filtered_args = { # Convert keys to lower-case. From 9fed65be94dd44052e37fb79cbc5c24e8fc9365b Mon Sep 17 00:00:00 2001 From: bayotte Date: Fri, 7 Aug 2026 10:57:36 -0400 Subject: [PATCH 2/3] adding tests --- ogc/test/test_servers.py | 104 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/ogc/test/test_servers.py b/ogc/test/test_servers.py index a1df634..6bc3423 100644 --- a/ogc/test/test_servers.py +++ b/ogc/test/test_servers.py @@ -47,6 +47,37 @@ def client(): yield app.test_client() +@pytest.fixture +def client_with_percent_and_caret_layer(): + """ + Create a test client for the Flask server whose layer identifier + contains a % and a ^, mirroring real-world layer names such as + "Relative_humidity_[%]" and "Total_precipitation_[kg/(m^2)]". + + Yields + ------ + client : FlaskClient + A test client for the Flask server. + """ + lat = np.linspace(90, -90, 11) + lon = np.linspace(-180, 180, 21) + data = np.random.default_rng(1).random((11, 21)) + coords = podpac.Coordinates([lat, lon], dims=["lat", "lon"]) + node = podpac.data.Array(source=data, coordinates=coords) + + layer = pogc.Layer( + node=node, + identifier="Total_precipitation_[kg/(m^2)]_[%]", + title="Total Precipitation", + abstract="Total Precipitation Data", + group="Layers", + ) + ogc = core.OGC(layers=[layer]) + app = servers.FlaskServer(__name__, ogcs=[ogc]) + app.config.update({"TESTING": True}) + yield app.test_client() + + @pytest.fixture def disable_all_formats_in_env(): """Setup the environmental variables for no supported formats.""" @@ -271,3 +302,76 @@ def raises_runtime_error(api_request, *args, **kwargs): response = client.get("/test_edr_exc") assert response.status_code == 500 assert "NoApplicableCode" in response.get_data(as_text=True) + + +def test_ogc_render_percent_and_caret_are_preserved(client): + """% and ^ are legitimate characters in layer identifiers (e.g. units + like "[%]" or "[kg/(m^2)]") and must survive the KV allowlist filter + used by the main WMS/WCS endpoint, while characters that are genuinely + outside the allowlist are still stripped.""" + captured_args = {} + original_handle_wms_kv = core.OGC.handle_wms_kv + + def capturing(self, args): + captured_args.update(args) + return original_handle_wms_kv(self, args) + + with patch.object(core.OGC, "handle_wms_kv", new=capturing): + client.get( + "/ogc", + query_string={ + "SERVICE": "WMS", + "REQUEST": "GetLegendGraphic", + "VERSION": "1.3.0", + "LAYER": "Relative_humidity_[%]^unsafe!@#$", + }, + ) + + layer_value = captured_args.get("layer", "") + # % and ^ must survive the filter... + assert "%" in layer_value + assert "^" in layer_value + # ...but characters outside the allowlist must still be stripped. + assert all(c not in layer_value for c in "!@#$") + + +def test_get_map_with_percent_and_caret_in_layer_name_returns_200(client_with_percent_and_caret_layer): + """Regression test: a real GetMap request against a layer whose + identifier contains % and ^ must succeed end-to-end, rather than + the identifier being corrupted by the allowlist filter and failing + the coverage lookup.""" + response = client_with_percent_and_caret_layer.get( + "/ogc", + query_string={ + "SERVICE": "WMS", + "REQUEST": "GetMap", + "VERSION": "1.3.0", + "LAYERS": "Total_precipitation_[kg/(m^2)]_[%]", + "CRS": "EPSG:4326", + "BBOX": "-180,-90,180,90", + "FORMAT": "image/png", + "HEIGHT": 512, + "WIDTH": 512, + }, + ) + assert response.status_code == 200 + assert response.content_type == "image/png" + + +def test_get_legend_graphic_with_percent_and_caret_in_layer_name_returns_200( + client_with_percent_and_caret_layer, +): + """Regression test: a real GetLegendGraphic request against a layer + whose identifier contains % and ^ must succeed end-to-end.""" + response = client_with_percent_and_caret_layer.get( + "/ogc", + query_string={ + "SERVICE": "WMS", + "REQUEST": "GetLegendGraphic", + "VERSION": "1.3.0", + "LAYER": "Total_precipitation_[kg/(m^2)]_[%]", + "FORMAT": "image/png", + }, + ) + assert response.status_code == 200 + assert response.content_type == "image/png" From 8b73c1dd34e2403a9dfaa8fdfffcda6014214304 Mon Sep 17 00:00:00 2001 From: bayotte Date: Fri, 7 Aug 2026 11:32:43 -0400 Subject: [PATCH 3/3] updating comment to include new allowed chars --- ogc/servers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ogc/servers.py b/ogc/servers.py index 0d3835e..781facb 100755 --- a/ogc/servers.py +++ b/ogc/servers.py @@ -301,7 +301,7 @@ def ogc_render(self, ogc_idx): # Note the parameter with key "params" has a serialized JSON value, # so we allow braces, brackets, and quotes. # Allowed chars are: - # -, A through Z, a through z, 0 through 9, + # -, A through Z, a through z, 0 through 9, ^, and % # and the characters + . , _ / : * { } ( ) [ ] " % ^ allowed_chars = r'-A-Za-z0-9+.,_/:*\{\}\(\)\[\]"%^' match_one_unallowed_char = "[^%s]" % allowed_chars @@ -387,7 +387,7 @@ def wrapper(*args, **kwargs) -> Response: # Note the parameter with key "params" has a serialized JSON value, # so we allow braces, brackets, and quotes. # Allowed chars are: - # -, A through Z, a through z, 0 through 9, spaces + # -, A through Z, a through z, 0 through 9, spaces, ^, and % # and the characters + . , _ / : * { } ( ) [ ] " allowed_chars = r'-A-Za-z0-9 +.,_/:*\{\}\(\)\[\]"%^' match_one_unallowed_char = "[^%s]" % allowed_chars