Skip to content

Commit 79f1ab5

Browse files
andystaplesCopilot
andcommitted
Validate forwarded management URL origins
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3ec5c077-6e8b-4cb3-916a-12bd3cb13937
1 parent b75223c commit 79f1ab5

3 files changed

Lines changed: 28 additions & 3 deletions

File tree

‎azure-functions-durable/azure/durable_functions/client.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,24 @@ def _get_request_origin(
6262
if separator:
6363
forwarded_values[name.strip().lower()] = value.strip().strip('"')
6464

65-
proto = forwarded_values.get("proto", proto)
65+
forwarded_proto = forwarded_values.get("proto")
66+
if forwarded_proto:
67+
proto = forwarded_proto
6668
forwarded_host = forwarded_values.get("host")
6769
if forwarded_host:
6870
return f"{proto}://{forwarded_host}"
6971

7072
forwarded_proto = headers.get("x-forwarded-proto")
7173
if forwarded_proto:
72-
proto = _first_forwarded_value(forwarded_proto)
74+
first_proto = _first_forwarded_value(forwarded_proto)
75+
if first_proto:
76+
proto = first_proto
7377

7478
forwarded_host = headers.get("x-forwarded-host")
7579
if forwarded_host:
76-
host = _first_forwarded_value(forwarded_host)
80+
first_host = _first_forwarded_value(forwarded_host)
81+
if first_host:
82+
host = first_host
7783

7884
return f"{proto}://{host}"
7985

‎azure-functions-durable/azure/durable_functions/http/http_management_payload.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ def replace_url_origin(url: str, request_origin: str | None) -> str:
8888

8989
parsed_url = urlsplit(url)
9090
parsed_origin = urlsplit(request_origin)
91+
if not parsed_origin.scheme or not parsed_origin.netloc:
92+
raise ValueError(
93+
"request_origin must include both a scheme and an authority")
9194
if not parsed_url.scheme or not parsed_url.netloc:
9295
return url
9396

‎tests/azure-functions-durable/test_client_compat.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
to_durabletask_status,
1717
to_durabletask_statuses,
1818
)
19+
from azure.durable_functions.http.http_management_payload import (
20+
replace_url_origin,
21+
)
1922
from durabletask.client import AsyncTaskHubGrpcClient, OrchestrationStatus
2023
from durabletask.entities import EntityInstanceId
2124
from durabletask.task import RetryPolicy
@@ -220,6 +223,10 @@ async def test_create_http_management_payload_requires_instance_id():
220223
"https://public.example:8443"),
221224
({"X-Forwarded-Proto": "https", "X-Forwarded-Host": "proxy.example"},
222225
"https://proxy.example"),
226+
({"Forwarded": "proto=;host=public.example"},
227+
"http://public.example"),
228+
({"X-Forwarded-Proto": ",https"},
229+
"http://request-internal:7071"),
223230
],
224231
)
225232
async def test_management_payload_uses_host_templates_and_external_origin(
@@ -291,6 +298,15 @@ async def test_management_payload_without_request_preserves_template_origin():
291298
await client.close()
292299

293300

301+
@pytest.mark.parametrize(
302+
"origin",
303+
["", "//public.example", "https:"],
304+
)
305+
def test_replace_url_origin_rejects_invalid_origin(origin):
306+
with pytest.raises(ValueError, match="scheme and an authority"):
307+
replace_url_origin("http://internal.example/custom/path", origin)
308+
309+
294310
async def test_host_config_uses_http_base_url_and_ignores_untrusted_forwarding():
295311
client = df.DurableFunctionsClient(_make_host_config())
296312
request = func.HttpRequest(

0 commit comments

Comments
 (0)