Skip to content

Commit f8371ea

Browse files
Strip sensitive auth info on cross-domain redirect
1 parent 347b00e commit f8371ea

4 files changed

Lines changed: 145 additions & 1 deletion

File tree

sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
StorageLoggingPolicy,
5656
StorageRequestHook,
5757
StorageResponseHook,
58+
StorageSensitiveHeaderCleanupPolicy,
5859
)
5960
from .request_handlers import serialize_batch_body, _get_batch_request_delimiter
6061
from .response_handlers import PartialBatchErrorException, process_storage_error
@@ -331,6 +332,7 @@ def _create_pipeline(
331332
StorageResponseHook(**kwargs),
332333
DistributedTracingPolicy(**kwargs),
333334
HttpLoggingPolicy(**kwargs),
335+
StorageSensitiveHeaderCleanupPolicy(**kwargs),
334336
]
335337
if kwargs.get("_additional_pipeline_policies"):
336338
policies = policies + kwargs.get("_additional_pipeline_policies") # type: ignore

sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client_async.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
StorageHeadersPolicy,
4040
StorageHosts,
4141
StorageRequestHook,
42+
StorageSensitiveHeaderCleanupPolicy,
4243
)
4344
from .policies_async import (
4445
AsyncStorageBearerTokenCredentialPolicy,
@@ -145,6 +146,7 @@ def _create_pipeline(
145146
AsyncStorageResponseHook(**kwargs),
146147
DistributedTracingPolicy(**kwargs),
147148
HttpLoggingPolicy(**kwargs),
149+
StorageSensitiveHeaderCleanupPolicy(**kwargs),
148150
]
149151
if kwargs.get("_additional_pipeline_policies"):
150152
policies = policies + kwargs.get("_additional_pipeline_policies") # type: ignore

sdk/storage/azure-storage-blob/azure/storage/blob/_shared/policies.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import uuid
1212
from io import BytesIO, SEEK_SET, UnsupportedOperation
1313
from time import time
14-
from typing import Any, Dict, Optional, TYPE_CHECKING, Union
14+
from typing import Any, Dict, List, Optional, TypeVar, TYPE_CHECKING, Union
1515
from urllib.parse import (
1616
parse_qsl,
1717
urlencode,
@@ -29,6 +29,12 @@
2929
RequestHistory,
3030
SansIOHTTPPolicy,
3131
)
32+
from azure.core.pipeline import PipelineRequest
33+
from azure.core.pipeline.transport import (
34+
HttpRequest as LegacyHttpRequest,
35+
HttpResponse as LegacyHttpResponse,
36+
)
37+
from azure.core.rest import HttpRequest, HttpResponse
3238

3339
from .authentication import AzureSigningError, StorageHttpChallenge
3440
from .constants import DEFAULT_OAUTH_SCOPE, DATA_BLOCK_SIZE
@@ -54,6 +60,10 @@
5460
)
5561

5662

63+
HTTPResponseType = TypeVar("HTTPResponseType", HttpResponse, LegacyHttpResponse)
64+
HTTPRequestType = TypeVar("HTTPRequestType", HttpRequest, LegacyHttpRequest)
65+
66+
5767
_LOGGER = logging.getLogger(__name__)
5868
CONTENT_LENGTH_HEADER = "Content-Length"
5969
MD5_HEADER = "Content-MD5"
@@ -843,3 +853,70 @@ def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse")
843853
self.authorize_request(request, scope, tenant_id=challenge.tenant_id)
844854

845855
return True
856+
857+
858+
class StorageSensitiveHeaderCleanupPolicy(SansIOHTTPPolicy[HTTPRequestType, HTTPResponseType]):
859+
"""A simple policy that cleans up sensitive headers
860+
861+
:keyword list[str] blocked_redirect_headers: The headers to clean up when redirecting to another domain.
862+
:keyword bool disable_redirect_cleanup: Opt out cleaning up sensitive headers when redirecting to another domain.
863+
"""
864+
865+
DEFAULT_SENSITIVE_HEADERS = set(
866+
[
867+
"Authorization",
868+
"x-ms-authorization-auxiliary",
869+
"x-ms-copy-source",
870+
"x-ms-copy-source-authorization",
871+
"x-ms-rename-source",
872+
]
873+
)
874+
875+
DEFAULT_SENSITIVE_QUERY_PARAMS = set(
876+
[
877+
"sig",
878+
]
879+
)
880+
881+
def __init__(
882+
self,
883+
*,
884+
blocked_redirect_headers: Optional[List[str]] = None,
885+
blocked_query_params: Optional[List[str]] = None,
886+
disable_redirect_cleanup: bool = False,
887+
**kwargs: Any
888+
) -> None:
889+
self._disable_redirect_cleanup = disable_redirect_cleanup
890+
self._blocked_redirect_headers = (
891+
StorageSensitiveHeaderCleanupPolicy.DEFAULT_SENSITIVE_HEADERS
892+
if blocked_redirect_headers is None
893+
else blocked_redirect_headers
894+
)
895+
self._blocked_query_params = (
896+
StorageSensitiveHeaderCleanupPolicy.DEFAULT_SENSITIVE_QUERY_PARAMS
897+
if blocked_query_params is None
898+
else blocked_query_params
899+
)
900+
901+
def on_request(self, request: PipelineRequest[HTTPRequestType]) -> None:
902+
"""This is executed before sending the request to the next policy.
903+
904+
:param request: The PipelineRequest object.
905+
:type request: ~azure.core.pipeline.PipelineRequest
906+
"""
907+
# "insecure_domain_change" is used to indicate that a redirect
908+
# has occurred to a different domain. This tells the SensitiveHeaderCleanupPolicy
909+
# to clean up sensitive headers.
910+
insecure_domain_change = request.context.get("insecure_domain_change", False)
911+
if not self._disable_redirect_cleanup and insecure_domain_change:
912+
# Clean up request query parameters
913+
parsed = urlparse(request.http_request.url)
914+
kept = [
915+
pair for pair in parsed.query.split("&")
916+
if pair and pair.split("=", 1)[0] not in self._blocked_query_params
917+
]
918+
request.http_request.url = urlunparse(parsed._replace(query="&".join(kept)))
919+
920+
# Clean up request headers
921+
for header in self._blocked_redirect_headers:
922+
request.http_request.headers.pop(header, None)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from azure.core.pipeline import PipelineRequest, PipelineContext
2+
from azure.core.rest import HttpRequest
3+
from azure.storage.blob._shared.policies import StorageSensitiveHeaderCleanupPolicy
4+
5+
6+
def make_request(url, headers=None):
7+
http_request = HttpRequest("GET", url, headers=headers or {})
8+
context = PipelineContext(None)
9+
return PipelineRequest(http_request, context)
10+
11+
12+
class TestStorageSensitiveHeaderCleanup:
13+
SENSITIVE_HEADERS = {
14+
"Authorization": "Bearer token",
15+
"x-ms-authorization-auxiliary": "Bearer aux-token",
16+
"x-ms-copy-source": "https://acct.blob.core.windows.net/c/src?sig=SECRET",
17+
"x-ms-copy-source-authorization": "Bearer copy-token",
18+
"x-ms-rename-source": "/c/old",
19+
}
20+
21+
def test_storage_sensitive_cleanup_on_redirect(self):
22+
headers = dict(self.SENSITIVE_HEADERS)
23+
headers["x-ms-meta-keep"] = "ok"
24+
request = make_request(
25+
"https://acct.blob.core.windows.net/c/b?comp=block&sv=2026-04-06&sig=SECRET",
26+
headers=headers,
27+
)
28+
request.context["insecure_domain_change"] = True
29+
30+
StorageSensitiveHeaderCleanupPolicy().on_request(request)
31+
32+
assert "sig=" not in request.http_request.url
33+
assert "sv=2026-04-06" in request.http_request.url
34+
assert "comp=block" in request.http_request.url
35+
36+
for header in self.SENSITIVE_HEADERS:
37+
assert header not in request.http_request.headers
38+
39+
assert request.http_request.headers["x-ms-meta-keep"] == "ok"
40+
41+
def test_no_cleanup_when_no_redirect(self):
42+
request = make_request(
43+
"https://acct.blob.core.windows.net/c/b?sig=SECRET",
44+
headers=dict(self.SENSITIVE_HEADERS),
45+
)
46+
StorageSensitiveHeaderCleanupPolicy().on_request(request)
47+
48+
assert "sig=SECRET" in request.http_request.url
49+
for header, value in self.SENSITIVE_HEADERS.items():
50+
assert request.http_request.headers[header] == value
51+
52+
def test_no_cleanup_when_disabled(self):
53+
request = make_request(
54+
"https://acct.blob.core.windows.net/c/b?sig=SECRET",
55+
headers=dict(self.SENSITIVE_HEADERS),
56+
)
57+
request.context["insecure_domain_change"] = True
58+
59+
StorageSensitiveHeaderCleanupPolicy(disable_redirect_cleanup=True).on_request(request)
60+
61+
assert "sig=SECRET" in request.http_request.url
62+
for header, value in self.SENSITIVE_HEADERS.items():
63+
assert request.http_request.headers[header] == value

0 commit comments

Comments
 (0)