Skip to content

Commit d066e9c

Browse files
authored
Merge pull request #511 from microsoft/cobando/fix-redirect-vulnerability
Fix: Adding header removal handler
2 parents f0a220d + 67bb784 commit d066e9c

File tree

3 files changed

+385
-57
lines changed

3 files changed

+385
-57
lines changed

packages/http/httpx/kiota_http/middleware/options/redirect_handler_option.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
1+
from typing import Callable, Optional
12
from kiota_abstractions.request_option import RequestOption
23

4+
import httpx
5+
6+
# Type alias for the scrub sensitive headers callback
7+
ScrubSensitiveHeadersCallback = Callable[[httpx.Request, httpx.URL], None]
8+
9+
10+
def default_scrub_sensitive_headers(new_request: httpx.Request, original_url: httpx.URL) -> None:
11+
"""
12+
The default implementation for scrubbing sensitive headers during redirects.
13+
This method removes Authorization and Cookie headers when the host, scheme, or port changes.
14+
Args:
15+
new_request: The new redirect request to modify
16+
original_url: The original request URL
17+
"""
18+
if not new_request or not original_url:
19+
return
20+
21+
new_url = new_request.url
22+
if not new_url:
23+
return
24+
25+
# Remove Authorization and Cookie headers if the request's scheme, host, or port changes
26+
is_different_origin = (
27+
original_url.host != new_url.host or original_url.scheme != new_url.scheme
28+
or original_url.port != new_url.port
29+
)
30+
31+
if is_different_origin:
32+
new_request.headers.pop("Authorization", None)
33+
new_request.headers.pop("Cookie", None)
34+
35+
# Note: Proxy-Authorization is not handled here as proxy configuration in httpx
36+
# is managed at the transport level and not accessible to middleware.
37+
# In environments where this matters, the proxy configuration should be managed
38+
# at the HTTP client level.
39+
340

441
class RedirectHandlerOption(RequestOption):
542

@@ -15,7 +52,8 @@ def __init__(
1552
self,
1653
max_redirect: int = DEFAULT_MAX_REDIRECT,
1754
should_redirect: bool = True,
18-
allow_redirect_on_scheme_change: bool = False
55+
allow_redirect_on_scheme_change: bool = False,
56+
scrub_sensitive_headers: Optional[ScrubSensitiveHeadersCallback] = None
1957
) -> None:
2058

2159
if max_redirect > self.MAX_MAX_REDIRECT:
@@ -28,6 +66,7 @@ def __init__(
2866
self._max_redirect = max_redirect
2967
self._should_redirect = should_redirect
3068
self._allow_redirect_on_scheme_change = allow_redirect_on_scheme_change
69+
self._scrub_sensitive_headers = scrub_sensitive_headers or default_scrub_sensitive_headers
3170

3271
@property
3372
def max_redirect(self):
@@ -59,6 +98,16 @@ def allow_redirect_on_scheme_change(self):
5998
def allow_redirect_on_scheme_change(self, value: bool):
6099
self._allow_redirect_on_scheme_change = value
61100

101+
@property
102+
def scrub_sensitive_headers(self) -> ScrubSensitiveHeadersCallback:
103+
"""The callback for scrubbing sensitive headers during redirects.
104+
Defaults to default_scrub_sensitive_headers."""
105+
return self._scrub_sensitive_headers
106+
107+
@scrub_sensitive_headers.setter
108+
def scrub_sensitive_headers(self, value: ScrubSensitiveHeadersCallback):
109+
self._scrub_sensitive_headers = value
110+
62111
@staticmethod
63112
def get_key() -> str:
64113
return RedirectHandlerOption.REDIRECT_HANDLER_OPTION_KEY

packages/http/httpx/kiota_http/middleware/redirect_handler.py

Lines changed: 18 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ class RedirectHandler(BaseMiddleware):
2626
}
2727
STATUS_CODE_SEE_OTHER: int = 303
2828
LOCATION_HEADER: str = "Location"
29-
AUTHORIZATION_HEADER: str = "Authorization"
3029

3130
def __init__(self, options: RedirectHandlerOption = RedirectHandlerOption()) -> None:
3231
super().__init__()
@@ -125,15 +124,31 @@ def _build_redirect_request(
125124
"""
126125
method = self._redirect_method(request, response)
127126
url = self._redirect_url(request, response, options)
128-
headers = self._redirect_headers(request, url, method)
129127
stream = self._redirect_stream(request, method)
128+
129+
# Create the new request with the redirect URL and original headers
130130
new_request = httpx.Request(
131131
method=method,
132132
url=url,
133-
headers=headers,
133+
headers=request.headers.copy(),
134134
stream=stream,
135135
extensions=request.extensions,
136136
)
137+
138+
# Scrub sensitive headers before following the redirect
139+
options.scrub_sensitive_headers(new_request, request.url)
140+
141+
# Update the Host header if not same origin
142+
if not self._same_origin(url, request.url):
143+
new_request.headers["Host"] = url.netloc.decode("ascii")
144+
145+
# Handle 303 See Other and other method changes
146+
if method != request.method and method == "GET":
147+
# If we've switched to a 'GET' request, strip any headers which
148+
# are only relevant to the request body.
149+
new_request.headers.pop("Content-Length", None)
150+
new_request.headers.pop("Transfer-Encoding", None)
151+
137152
if hasattr(request, "context"):
138153
new_request.context = request.context #type: ignore
139154
new_request.options = {} #type: ignore
@@ -201,35 +216,6 @@ def _redirect_url(
201216

202217
return url
203218

204-
def _redirect_headers(
205-
self, request: httpx.Request, url: httpx.URL, method: str
206-
) -> httpx.Headers:
207-
"""
208-
Return the headers that should be used for the redirect request.
209-
"""
210-
headers = httpx.Headers(request.headers)
211-
212-
if not self._same_origin(url, request.url):
213-
if not self.is_https_redirect(request.url, url):
214-
# Strip Authorization headers when responses are redirected
215-
# away from the origin. (Except for direct HTTP to HTTPS redirects.)
216-
headers.pop("Authorization", None)
217-
218-
# Update the Host header.
219-
headers["Host"] = url.netloc.decode("ascii")
220-
221-
if method != request.method and method == "GET":
222-
# If we've switch to a 'GET' request, then strip any headers which
223-
# are only relevant to the request body.
224-
headers.pop("Content-Length", None)
225-
headers.pop("Transfer-Encoding", None)
226-
227-
# We should use the client cookie store to determine any cookie header,
228-
# rather than whatever was on the original outgoing request.
229-
headers.pop("Cookie", None)
230-
231-
return headers
232-
233219
def _redirect_stream(
234220
self, request: httpx.Request, method: str
235221
) -> typing.Optional[typing.Union[httpx.SyncByteStream, httpx.AsyncByteStream]]:
@@ -246,17 +232,3 @@ def _same_origin(self, url: httpx.URL, other: httpx.URL) -> bool:
246232
Return 'True' if the given URLs share the same origin.
247233
"""
248234
return (url.scheme == other.scheme and url.host == other.host)
249-
250-
def port_or_default(self, url: httpx.URL) -> typing.Optional[int]:
251-
if url.port is not None:
252-
return url.port
253-
return {"http": 80, "https": 443}.get(url.scheme)
254-
255-
def is_https_redirect(self, url: httpx.URL, location: httpx.URL) -> bool:
256-
"""
257-
Return 'True' if 'location' is a HTTPS upgrade of 'url'
258-
"""
259-
if url.host != location.host:
260-
return False
261-
262-
return (url.scheme == "http" and location.scheme == "https")

0 commit comments

Comments
 (0)