Skip to content

Commit ae302ad

Browse files
[Storage] Strip sensitive auth info on cross-domain redirect (#47541)
1 parent fc3e71c commit ae302ad

13 files changed

Lines changed: 331 additions & 4 deletions

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: 63 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, TYPE_CHECKING, Union
1515
from urllib.parse import (
1616
parse_qsl,
1717
urlencode,
@@ -843,3 +843,65 @@ def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse")
843843
self.authorize_request(request, scope, tenant_id=challenge.tenant_id)
844844

845845
return True
846+
847+
848+
class StorageSensitiveHeaderCleanupPolicy(SansIOHTTPPolicy):
849+
"""A simple policy that cleans up sensitive headers
850+
851+
:keyword list[str] blocked_redirect_headers: The headers to clean up when redirecting to another domain.
852+
:keyword bool disable_redirect_cleanup: Opt out cleaning up sensitive headers when redirecting to another domain.
853+
"""
854+
855+
DEFAULT_SENSITIVE_HEADERS = {
856+
"Authorization",
857+
"x-ms-authorization-auxiliary",
858+
"x-ms-copy-source",
859+
"x-ms-copy-source-authorization",
860+
"x-ms-rename-source",
861+
}
862+
863+
DEFAULT_SENSITIVE_QUERY_PARAMS = {"sig"}
864+
865+
def __init__(
866+
self, # pylint: disable=unused-argument
867+
*,
868+
blocked_redirect_headers: Optional[List[str]] = None,
869+
blocked_redirect_query_params: Optional[List[str]] = None,
870+
disable_redirect_cleanup: bool = False,
871+
**kwargs: Any,
872+
) -> None:
873+
self._disable_redirect_cleanup = disable_redirect_cleanup
874+
self._blocked_redirect_headers = (
875+
StorageSensitiveHeaderCleanupPolicy.DEFAULT_SENSITIVE_HEADERS
876+
if blocked_redirect_headers is None
877+
else blocked_redirect_headers
878+
)
879+
self._blocked_query_params = (
880+
StorageSensitiveHeaderCleanupPolicy.DEFAULT_SENSITIVE_QUERY_PARAMS
881+
if blocked_redirect_query_params is None
882+
else blocked_redirect_query_params
883+
)
884+
885+
def on_request(self, request: "PipelineRequest") -> None:
886+
"""This is executed before sending the request to the next policy.
887+
888+
:param request: The PipelineRequest object.
889+
:type request: ~azure.core.pipeline.PipelineRequest
890+
"""
891+
# "insecure_domain_change" is used to indicate that a redirect
892+
# has occurred to a different domain. This tells the SensitiveHeaderCleanupPolicy
893+
# to clean up sensitive headers.
894+
insecure_domain_change = request.context.get("insecure_domain_change", False)
895+
if not self._disable_redirect_cleanup and insecure_domain_change:
896+
# Clean up request query parameters
897+
parsed = urlparse(request.http_request.url)
898+
kept = [
899+
pair
900+
for pair in parsed.query.split("&")
901+
if pair and pair.split("=", 1)[0] not in self._blocked_query_params
902+
]
903+
request.http_request.url = urlunparse(parsed._replace(query="&".join(kept)))
904+
905+
# Clean up request headers
906+
for header in self._blocked_redirect_headers:
907+
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

sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_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
@@ -333,6 +334,7 @@ def _create_pipeline(
333334
StorageResponseHook(**kwargs),
334335
DistributedTracingPolicy(**kwargs),
335336
HttpLoggingPolicy(**kwargs),
337+
StorageSensitiveHeaderCleanupPolicy(**kwargs),
336338
]
337339
if kwargs.get("_additional_pipeline_policies"):
338340
policies = policies + kwargs.get("_additional_pipeline_policies") # type: ignore

sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_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-file-datalake/azure/storage/filedatalake/_shared/policies.py

Lines changed: 63 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, TYPE_CHECKING, Union
1515
from urllib.parse import (
1616
parse_qsl,
1717
urlencode,
@@ -843,3 +843,65 @@ def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse")
843843
self.authorize_request(request, scope, tenant_id=challenge.tenant_id)
844844

845845
return True
846+
847+
848+
class StorageSensitiveHeaderCleanupPolicy(SansIOHTTPPolicy):
849+
"""A simple policy that cleans up sensitive headers
850+
851+
:keyword list[str] blocked_redirect_headers: The headers to clean up when redirecting to another domain.
852+
:keyword bool disable_redirect_cleanup: Opt out cleaning up sensitive headers when redirecting to another domain.
853+
"""
854+
855+
DEFAULT_SENSITIVE_HEADERS = {
856+
"Authorization",
857+
"x-ms-authorization-auxiliary",
858+
"x-ms-copy-source",
859+
"x-ms-copy-source-authorization",
860+
"x-ms-rename-source",
861+
}
862+
863+
DEFAULT_SENSITIVE_QUERY_PARAMS = {"sig"}
864+
865+
def __init__(
866+
self, # pylint: disable=unused-argument
867+
*,
868+
blocked_redirect_headers: Optional[List[str]] = None,
869+
blocked_redirect_query_params: Optional[List[str]] = None,
870+
disable_redirect_cleanup: bool = False,
871+
**kwargs: Any,
872+
) -> None:
873+
self._disable_redirect_cleanup = disable_redirect_cleanup
874+
self._blocked_redirect_headers = (
875+
StorageSensitiveHeaderCleanupPolicy.DEFAULT_SENSITIVE_HEADERS
876+
if blocked_redirect_headers is None
877+
else blocked_redirect_headers
878+
)
879+
self._blocked_query_params = (
880+
StorageSensitiveHeaderCleanupPolicy.DEFAULT_SENSITIVE_QUERY_PARAMS
881+
if blocked_redirect_query_params is None
882+
else blocked_redirect_query_params
883+
)
884+
885+
def on_request(self, request: "PipelineRequest") -> None:
886+
"""This is executed before sending the request to the next policy.
887+
888+
:param request: The PipelineRequest object.
889+
:type request: ~azure.core.pipeline.PipelineRequest
890+
"""
891+
# "insecure_domain_change" is used to indicate that a redirect
892+
# has occurred to a different domain. This tells the SensitiveHeaderCleanupPolicy
893+
# to clean up sensitive headers.
894+
insecure_domain_change = request.context.get("insecure_domain_change", False)
895+
if not self._disable_redirect_cleanup and insecure_domain_change:
896+
# Clean up request query parameters
897+
parsed = urlparse(request.http_request.url)
898+
kept = [
899+
pair
900+
for pair in parsed.query.split("&")
901+
if pair and pair.split("=", 1)[0] not in self._blocked_query_params
902+
]
903+
request.http_request.url = urlunparse(parsed._replace(query="&".join(kept)))
904+
905+
# Clean up request headers
906+
for header in self._blocked_redirect_headers:
907+
request.http_request.headers.pop(header, None)

sdk/storage/azure-storage-file-share/azure/storage/fileshare/_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
@@ -333,6 +334,7 @@ def _create_pipeline(
333334
StorageResponseHook(**kwargs),
334335
DistributedTracingPolicy(**kwargs),
335336
HttpLoggingPolicy(**kwargs),
337+
StorageSensitiveHeaderCleanupPolicy(**kwargs),
336338
]
337339
if kwargs.get("_additional_pipeline_policies"):
338340
policies = policies + kwargs.get("_additional_pipeline_policies") # type: ignore

sdk/storage/azure-storage-file-share/azure/storage/fileshare/_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-file-share/azure/storage/fileshare/_shared/policies.py

Lines changed: 63 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, TYPE_CHECKING, Union
1515
from urllib.parse import (
1616
parse_qsl,
1717
urlencode,
@@ -843,3 +843,65 @@ def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse")
843843
self.authorize_request(request, scope, tenant_id=challenge.tenant_id)
844844

845845
return True
846+
847+
848+
class StorageSensitiveHeaderCleanupPolicy(SansIOHTTPPolicy):
849+
"""A simple policy that cleans up sensitive headers
850+
851+
:keyword list[str] blocked_redirect_headers: The headers to clean up when redirecting to another domain.
852+
:keyword bool disable_redirect_cleanup: Opt out cleaning up sensitive headers when redirecting to another domain.
853+
"""
854+
855+
DEFAULT_SENSITIVE_HEADERS = {
856+
"Authorization",
857+
"x-ms-authorization-auxiliary",
858+
"x-ms-copy-source",
859+
"x-ms-copy-source-authorization",
860+
"x-ms-rename-source",
861+
}
862+
863+
DEFAULT_SENSITIVE_QUERY_PARAMS = {"sig"}
864+
865+
def __init__(
866+
self, # pylint: disable=unused-argument
867+
*,
868+
blocked_redirect_headers: Optional[List[str]] = None,
869+
blocked_redirect_query_params: Optional[List[str]] = None,
870+
disable_redirect_cleanup: bool = False,
871+
**kwargs: Any,
872+
) -> None:
873+
self._disable_redirect_cleanup = disable_redirect_cleanup
874+
self._blocked_redirect_headers = (
875+
StorageSensitiveHeaderCleanupPolicy.DEFAULT_SENSITIVE_HEADERS
876+
if blocked_redirect_headers is None
877+
else blocked_redirect_headers
878+
)
879+
self._blocked_query_params = (
880+
StorageSensitiveHeaderCleanupPolicy.DEFAULT_SENSITIVE_QUERY_PARAMS
881+
if blocked_redirect_query_params is None
882+
else blocked_redirect_query_params
883+
)
884+
885+
def on_request(self, request: "PipelineRequest") -> None:
886+
"""This is executed before sending the request to the next policy.
887+
888+
:param request: The PipelineRequest object.
889+
:type request: ~azure.core.pipeline.PipelineRequest
890+
"""
891+
# "insecure_domain_change" is used to indicate that a redirect
892+
# has occurred to a different domain. This tells the SensitiveHeaderCleanupPolicy
893+
# to clean up sensitive headers.
894+
insecure_domain_change = request.context.get("insecure_domain_change", False)
895+
if not self._disable_redirect_cleanup and insecure_domain_change:
896+
# Clean up request query parameters
897+
parsed = urlparse(request.http_request.url)
898+
kept = [
899+
pair
900+
for pair in parsed.query.split("&")
901+
if pair and pair.split("=", 1)[0] not in self._blocked_query_params
902+
]
903+
request.http_request.url = urlunparse(parsed._replace(query="&".join(kept)))
904+
905+
# Clean up request headers
906+
for header in self._blocked_redirect_headers:
907+
request.http_request.headers.pop(header, None)

0 commit comments

Comments
 (0)