Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions ogc/servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,9 @@ 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,
# and the characters + . , _ / : * { } ( ) [ ] "
allowed_chars = r'-A-Za-z0-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
args = {
# WCS standard says argument keys can come in with any
Expand Down Expand Up @@ -387,9 +387,9 @@ 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 + . , _ / : * { } ( ) [ ] "
Comment thread
BAyotte marked this conversation as resolved.
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.
Expand Down
104 changes: 104 additions & 0 deletions ogc/test/test_servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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"
Loading