Skip to content

Commit 4e50fbc

Browse files
authored
Merge pull request from GHSA-g4mx-q9vg-27p4
1 parent 80808b0 commit 4e50fbc

6 files changed

Lines changed: 60 additions & 3 deletions

File tree

dummyserver/handlers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ def encodingrequest(self, request: httputil.HTTPServerRequest) -> Response:
281281
def headers(self, request: httputil.HTTPServerRequest) -> Response:
282282
return Response(json.dumps(dict(request.headers)))
283283

284+
def headers_and_params(self, request: httputil.HTTPServerRequest) -> Response:
285+
params = request_params(request)
286+
return Response(
287+
json.dumps({"headers": dict(request.headers), "params": params})
288+
)
289+
284290
def multi_headers(self, request: httputil.HTTPServerRequest) -> Response:
285291
return Response(json.dumps({"headers": list(request.headers.get_all())}))
286292

src/urllib3/_collections.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
if typing.TYPE_CHECKING:
99
# We can only import Protocol if TYPE_CHECKING because it's a development
1010
# dependency, and is not available at runtime.
11-
from typing_extensions import Protocol
11+
from typing_extensions import Protocol, Self
1212

1313
class HasGettableStringKeys(Protocol):
1414
def keys(self) -> typing.Iterator[str]:
@@ -391,6 +391,24 @@ def getlist(
391391
# meets our external interface requirement of `Union[List[str], _DT]`.
392392
return vals[1:]
393393

394+
def _prepare_for_method_change(self) -> Self:
395+
"""
396+
Remove content-specific header fields before changing the request
397+
method to GET or HEAD according to RFC 9110, Section 15.4.
398+
"""
399+
content_specific_headers = [
400+
"Content-Encoding",
401+
"Content-Language",
402+
"Content-Location",
403+
"Content-Type",
404+
"Content-Length",
405+
"Digest",
406+
"Last-Modified",
407+
]
408+
for header in content_specific_headers:
409+
self.discard(header)
410+
return self
411+
394412
# Backwards compatibility for httplib
395413
getheaders = getlist
396414
getallmatchingheaders = getlist

src/urllib3/connectionpool.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from types import TracebackType
1212

1313
from ._base_connection import _TYPE_BODY
14+
from ._collections import HTTPHeaderDict
1415
from ._request_methods import RequestMethods
1516
from .connection import (
1617
BaseSSLError,
@@ -893,7 +894,11 @@ def urlopen( # type: ignore[override]
893894
redirect_location = redirect and response.get_redirect_location()
894895
if redirect_location:
895896
if response.status == 303:
897+
# Change the method according to RFC 9110, Section 15.4.4.
896898
method = "GET"
899+
# And lose the body not to transfer anything sensitive.
900+
body = None
901+
headers = HTTPHeaderDict(headers)._prepare_for_method_change()
897902

898903
try:
899904
retries = retries.increment(method, url, response=response, _pool=self)

src/urllib3/poolmanager.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from types import TracebackType
88
from urllib.parse import urljoin
99

10-
from ._collections import RecentlyUsedContainer
10+
from ._collections import HTTPHeaderDict, RecentlyUsedContainer
1111
from ._request_methods import RequestMethods
1212
from .connection import ProxyConfig
1313
from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme
@@ -449,9 +449,12 @@ def urlopen( # type: ignore[override]
449449
# Support relative URLs for redirecting.
450450
redirect_location = urljoin(url, redirect_location)
451451

452-
# RFC 7231, Section 6.4.4
453452
if response.status == 303:
453+
# Change the method according to RFC 9110, Section 15.4.4.
454454
method = "GET"
455+
# And lose the body not to transfer anything sensitive.
456+
kw["body"] = None
457+
kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change()
455458

456459
retries = kw.get("retries")
457460
if not isinstance(retries, Retry):

test/with_dummyserver/test_connectionpool.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,17 @@ def test_redirect(self) -> None:
480480
assert r.status == 200
481481
assert r.data == b"Dummy server!"
482482

483+
def test_303_redirect_makes_request_lose_body(self) -> None:
484+
with HTTPConnectionPool(self.host, self.port) as pool:
485+
response = pool.request(
486+
"POST",
487+
"/redirect",
488+
fields={"target": "/headers_and_params", "status": "303 See Other"},
489+
)
490+
data = response.json()
491+
assert data["params"] == {}
492+
assert "Content-Type" not in HTTPHeaderDict(data["headers"])
493+
483494
def test_bad_connect(self) -> None:
484495
with HTTPConnectionPool("badhost.invalid", self.port) as pool:
485496
with pytest.raises(MaxRetryError) as e:

test/with_dummyserver/test_poolmanager.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,20 @@ def test_redirect_without_preload_releases_connection(self) -> None:
244244
assert r._pool.num_connections == 1
245245
assert len(http.pools) == 1
246246

247+
def test_303_redirect_makes_request_lose_body(self) -> None:
248+
with PoolManager() as http:
249+
response = http.request(
250+
"POST",
251+
f"{self.base_url}/redirect",
252+
fields={
253+
"target": f"{self.base_url}/headers_and_params",
254+
"status": "303 See Other",
255+
},
256+
)
257+
data = response.json()
258+
assert data["params"] == {}
259+
assert "Content-Type" not in HTTPHeaderDict(data["headers"])
260+
247261
def test_unknown_scheme(self) -> None:
248262
with PoolManager() as http:
249263
unknown_scheme = "unknown"

0 commit comments

Comments
 (0)