Skip to content

Commit 7ce0262

Browse files
Strip custom credential headers on cross-host redirects
requests.Session.rebuild_auth() strips the standard sensitive headers (Authorization, Cookie) when a redirect changes host, but not the custom Databricks cloud-provider token headers (X-Databricks-Azure-SP-Management-Token, X-Databricks-GCP-SA-Access-Token). On a cross-host redirect those cloud tokens were forwarded to the redirect target host even though Authorization was stripped. Use a requests.Session subclass whose rebuild_auth drops these headers on the same host-change condition (should_strip_auth), matching how requests handles Authorization. Same-host redirects are unaffected. Signed-off-by: winstoncrooker <winstoncrooker@outlook.com>
1 parent e25671d commit 7ce0262

2 files changed

Lines changed: 77 additions & 1 deletion

File tree

databricks/sdk/_base_client.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,29 @@ def _fix_host_if_needed(host: Optional[str]) -> Optional[str]:
3838
return urllib.parse.urlunparse((o.scheme, netloc, path, o.params, o.query, o.fragment))
3939

4040

41+
class _CredentialStrippingSession(requests.Session):
42+
"""A requests session that also strips Databricks credential headers on a cross-host redirect.
43+
44+
requests.Session.rebuild_auth() already removes the standard sensitive headers
45+
(Authorization, Cookie, etc.) when a redirect changes host. The Databricks cloud-provider
46+
token headers are custom, so requests does not know to strip them. Extend rebuild_auth to
47+
drop them on the same host-change condition, so a cross-host redirect cannot forward the
48+
caller's Azure/GCP token to a different host.
49+
"""
50+
51+
_sensitive_cross_host_headers = (
52+
"X-Databricks-Azure-SP-Management-Token",
53+
"X-Databricks-GCP-SA-Access-Token",
54+
"X-Databricks-Azure-Workspace-Resource-Id",
55+
)
56+
57+
def rebuild_auth(self, prepared_request, response):
58+
super().rebuild_auth(prepared_request, response)
59+
if self.should_strip_auth(response.request.url, prepared_request.url):
60+
for header in self._sensitive_cross_host_headers:
61+
prepared_request.headers.pop(header, None)
62+
63+
4164
class _BaseClient:
4265
def __init__(
4366
self,
@@ -80,7 +103,7 @@ def __init__(
80103
self._user_agent_base = user_agent_base or useragent.to_string()
81104
self._header_factory = header_factory
82105
self._clock = clock or RealClock()
83-
self._session = requests.Session()
106+
self._session = _CredentialStrippingSession()
84107
self._session.auth = self._authenticate
85108
self._streaming_buffer_size = streaming_buffer_size
86109

tests/test_base_client.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,3 +561,56 @@ def do():
561561
do()
562562

563563
assert session._received_requests == [test_case._expected_result for _ in range(expected_attempts_made)]
564+
565+
566+
def _prep(url, headers):
567+
req = PreparedRequest()
568+
req.prepare(method="GET", url=url, headers=headers)
569+
return req
570+
571+
572+
def test_credential_stripping_session_drops_cloud_tokens_on_cross_host_redirect():
573+
from databricks.sdk._base_client import _CredentialStrippingSession
574+
575+
session = _CredentialStrippingSession()
576+
original = _prep("https://tenant.cloud.databricks.com/api/2.0/preview/scim/v2/Me", {})
577+
response = Response()
578+
response.request = original
579+
580+
redirected = _prep(
581+
"https://attacker.example/api/2.0/preview/scim/v2/Me",
582+
{
583+
"Authorization": "Bearer pat-token",
584+
"X-Databricks-Azure-SP-Management-Token": "azure-arm-token",
585+
"X-Databricks-GCP-SA-Access-Token": "gcp-sa-token",
586+
},
587+
)
588+
session.rebuild_auth(redirected, response)
589+
590+
# requests strips Authorization on host change; we additionally strip the custom cloud headers.
591+
assert "Authorization" not in redirected.headers
592+
assert "X-Databricks-Azure-SP-Management-Token" not in redirected.headers
593+
assert "X-Databricks-GCP-SA-Access-Token" not in redirected.headers
594+
595+
596+
def test_credential_stripping_session_keeps_cloud_tokens_on_same_host_redirect():
597+
from databricks.sdk._base_client import _CredentialStrippingSession
598+
599+
session = _CredentialStrippingSession()
600+
original = _prep("https://tenant.cloud.databricks.com/first", {})
601+
response = Response()
602+
response.request = original
603+
604+
redirected = _prep(
605+
"https://tenant.cloud.databricks.com/second",
606+
{
607+
"Authorization": "Bearer pat-token",
608+
"X-Databricks-Azure-SP-Management-Token": "azure-arm-token",
609+
"X-Databricks-GCP-SA-Access-Token": "gcp-sa-token",
610+
},
611+
)
612+
session.rebuild_auth(redirected, response)
613+
614+
assert redirected.headers["Authorization"] == "Bearer pat-token"
615+
assert redirected.headers["X-Databricks-Azure-SP-Management-Token"] == "azure-arm-token"
616+
assert redirected.headers["X-Databricks-GCP-SA-Access-Token"] == "gcp-sa-token"

0 commit comments

Comments
 (0)