From 76ea54218833ecc20746682051f1d4e638e64ea5 Mon Sep 17 00:00:00 2001 From: "Durgesh Suryawanshi (Centific Technologies Inc)" Date: Wed, 25 Mar 2026 21:11:38 +0530 Subject: [PATCH 1/2] Added recordingUrl validator. --- .../callautomation/_content_downloader.py | 7 +++ .../_shared/recording_url_validator.py | 60 +++++++++++++++++++ .../aio/_content_downloader_async.py | 7 +++ 3 files changed, 74 insertions(+) create mode 100644 sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_shared/recording_url_validator.py diff --git a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_content_downloader.py b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_content_downloader.py index 4598dcced201..b9a9a49f0c15 100644 --- a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_content_downloader.py +++ b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_content_downloader.py @@ -22,6 +22,7 @@ from ._generated import models as _models from ._generated._utils.serialization import Serializer from ._generated.operations import CallRecordingOperations +from ._shared.recording_url_validator import validate_recording_url _SERIALIZER = Serializer() _SERIALIZER.client_side_validation = False @@ -64,6 +65,9 @@ def download_streaming( if not parsed_hostname: raise ValueError("Recording client endpoint must not be None.") + # Validate recording URL before sending authenticated request + validate_recording_url(source_location, "source_location") + _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} request = _build_call_recording_download_recording_request( @@ -111,6 +115,9 @@ def delete_recording(self, recording_location: str, **kwargs: Any) -> None: if not parsed_hostname: raise ValueError("Recording client endpoint must not be None.") + # Validate recording URL before sending authenticated request + validate_recording_url(recording_location, "recording_location") + _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} request = _build_call_recording_delete_recording_request( diff --git a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_shared/recording_url_validator.py b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_shared/recording_url_validator.py new file mode 100644 index 000000000000..8bc936b64bd0 --- /dev/null +++ b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_shared/recording_url_validator.py @@ -0,0 +1,60 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- + +from typing import Tuple +from urllib.parse import urlparse + + +# Allowed recording endpoint host suffixes. +# These are the only domains permitted for recording URLs to prevent credential exfiltration. +ALLOWED_HOST_SUFFIXES: Tuple[str, ...] = ( + ".asm.skype.com", + ".asyncgw.teams.microsoft.com", + ".blob.core.windows.net", +) + + +def validate_recording_url(recording_url: str, parameter_name: str) -> None: + """ + Validate that a recording URL points to Azure Communication Services + or Azure Blob Storage endpoint before credentials are attached. + This prevents credential exfiltration via SSRF attacks. + + :param recording_url: The recording URL to validate. + :type recording_url: str + :param parameter_name: The parameter name for exception messages. + :type parameter_name: str + :raises TypeError: If the recording URL is None or empty. + :raises ValueError: If the recording URL is not a valid absolute HTTPS URI or not from an allowed domain. + """ + if not recording_url: + raise TypeError(f"{parameter_name} cannot be null or undefined.") + + parsed_url = urlparse(recording_url) + + # Validate it's a valid absolute URI + if not parsed_url.scheme or not parsed_url.netloc: + raise ValueError(f"{parameter_name} must be a valid absolute URI.") + + # Ensure the URL uses HTTPS + if parsed_url.scheme.lower() != "https": + raise ValueError(f"{parameter_name} must use HTTPS scheme for security.") + + host = parsed_url.hostname + if not host: + raise ValueError(f"{parameter_name} must have a valid hostname.") + + host_lower = host.lower() + + # Check against allowed suffixes + is_valid_endpoint = any(host_lower.endswith(suffix) for suffix in ALLOWED_HOST_SUFFIXES) + + if not is_valid_endpoint: + raise ValueError( + f"{parameter_name} host '{host}' is not a valid Azure Communication Services recording endpoint. " + "Only URLs pointing to *.asm.skype.com, *.asyncgw.teams.microsoft.com, " + "or Azure Blob Storage (*.blob.core.windows.net) are allowed." + ) diff --git a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_content_downloader_async.py b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_content_downloader_async.py index d0deae02ea66..2e0a21f1bfe2 100644 --- a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_content_downloader_async.py +++ b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/aio/_content_downloader_async.py @@ -22,6 +22,7 @@ from .._generated import models as _models from .._generated._utils.serialization import Serializer from .._generated.aio.operations import CallRecordingOperations +from .._shared.recording_url_validator import validate_recording_url _SERIALIZER = Serializer() _SERIALIZER.client_side_validation = False @@ -65,6 +66,9 @@ async def download_streaming( if not parsed_hostname: raise ValueError("Recording client endpoint must not be None.") + # Validate recording URL before sending authenticated request + validate_recording_url(source_location, "source_location") + _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} request = _build_call_recording_download_recording_request( @@ -113,6 +117,9 @@ async def delete_recording(self, recording_location: str, **kwargs: Any) -> None if not parsed_hostname: raise ValueError("Recording client endpoint must not be None.") + # Validate recording URL before sending authenticated request + validate_recording_url(recording_location, "recording_location") + _headers = kwargs.pop("headers", {}) or {} _params = kwargs.pop("params", {}) or {} request = _build_call_recording_delete_recording_request( From e6a82261968ededec64154b1cbe16baaf89d06a1 Mon Sep 17 00:00:00 2001 From: "Durgesh Suryawanshi (Centific Technologies Inc)" Date: Thu, 26 Mar 2026 11:41:14 +0530 Subject: [PATCH 2/2] Updated code. --- .../callautomation/_shared/recording_url_validator.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_shared/recording_url_validator.py b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_shared/recording_url_validator.py index 8bc936b64bd0..c8867b8d67e7 100644 --- a/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_shared/recording_url_validator.py +++ b/sdk/communication/azure-communication-callautomation/azure/communication/callautomation/_shared/recording_url_validator.py @@ -13,14 +13,13 @@ ALLOWED_HOST_SUFFIXES: Tuple[str, ...] = ( ".asm.skype.com", ".asyncgw.teams.microsoft.com", - ".blob.core.windows.net", ) def validate_recording_url(recording_url: str, parameter_name: str) -> None: """ Validate that a recording URL points to Azure Communication Services - or Azure Blob Storage endpoint before credentials are attached. + endpoint before credentials are attached. This prevents credential exfiltration via SSRF attacks. :param recording_url: The recording URL to validate. @@ -55,6 +54,5 @@ def validate_recording_url(recording_url: str, parameter_name: str) -> None: if not is_valid_endpoint: raise ValueError( f"{parameter_name} host '{host}' is not a valid Azure Communication Services recording endpoint. " - "Only URLs pointing to *.asm.skype.com, *.asyncgw.teams.microsoft.com, " - "or Azure Blob Storage (*.blob.core.windows.net) are allowed." + "Only URLs pointing to *.asm.skype.com, *.asyncgw.teams.microsoft.com are allowed." )