Skip to content

Commit b594c5c

Browse files
authored
Merge pull request from GHSA-g4mx-q9vg-27p4
1 parent 944f0eb commit b594c5c

6 files changed

Lines changed: 61 additions & 2 deletions

File tree

dummyserver/handlers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ def redirect(self, request):
186186
status = request.params.get("status", "303 See Other")
187187
if len(status) == 3:
188188
status = "%s Redirect" % status.decode("latin-1")
189+
elif isinstance(status, bytes):
190+
status = status.decode("latin-1")
189191

190192
headers = [("Location", target)]
191193
return Response(status=status, headers=headers)
@@ -264,6 +266,11 @@ def encodingrequest(self, request):
264266
def headers(self, request):
265267
return Response(json.dumps(dict(request.headers)))
266268

269+
def headers_and_params(self, request):
270+
return Response(
271+
json.dumps({"headers": dict(request.headers), "params": request.params})
272+
)
273+
267274
def successful_retry(self, request):
268275
"""Handler which will return an error and then success
269276

src/urllib3/_collections.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,24 @@ def getlist(self, key, default=__marker):
268268
else:
269269
return vals[1:]
270270

271+
def _prepare_for_method_change(self):
272+
"""
273+
Remove content-specific header fields before changing the request
274+
method to GET or HEAD according to RFC 9110, Section 15.4.
275+
"""
276+
content_specific_headers = [
277+
"Content-Encoding",
278+
"Content-Language",
279+
"Content-Location",
280+
"Content-Type",
281+
"Content-Length",
282+
"Digest",
283+
"Last-Modified",
284+
]
285+
for header in content_specific_headers:
286+
self.discard(header)
287+
return self
288+
271289
# Backwards compatibility for httplib
272290
getheaders = getlist
273291
getallmatchingheaders = getlist

src/urllib3/connectionpool.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from socket import error as SocketError
1010
from socket import timeout as SocketTimeout
1111

12+
from ._collections import HTTPHeaderDict
1213
from .connection import (
1314
BaseSSLError,
1415
BrokenPipeError,
@@ -843,7 +844,11 @@ def _is_ssl_error_message_from_http_proxy(ssl_error):
843844
redirect_location = redirect and response.get_redirect_location()
844845
if redirect_location:
845846
if response.status == 303:
847+
# Change the method according to RFC 9110, Section 15.4.4.
846848
method = "GET"
849+
# And lose the body not to transfer anything sensitive.
850+
body = None
851+
headers = HTTPHeaderDict(headers)._prepare_for_method_change()
847852

848853
try:
849854
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
@@ -4,7 +4,7 @@
44
import functools
55
import logging
66

7-
from ._collections import RecentlyUsedContainer
7+
from ._collections import HTTPHeaderDict, RecentlyUsedContainer
88
from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme
99
from .exceptions import (
1010
LocationValueError,
@@ -382,9 +382,12 @@ def urlopen(self, method, url, redirect=True, **kw):
382382
# Support relative URLs for redirecting.
383383
redirect_location = urljoin(url, redirect_location)
384384

385-
# RFC 7231, Section 6.4.4
386385
if response.status == 303:
386+
# Change the method according to RFC 9110, Section 15.4.4.
387387
method = "GET"
388+
# And lose the body not to transfer anything sensitive.
389+
kw["body"] = None
390+
kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change()
388391

389392
retries = kw.get("retries")
390393
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
@@ -464,6 +464,17 @@ def test_redirect(self):
464464
assert r.status == 200
465465
assert r.data == b"Dummy server!"
466466

467+
def test_303_redirect_makes_request_lose_body(self):
468+
with HTTPConnectionPool(self.host, self.port) as pool:
469+
response = pool.request(
470+
"POST",
471+
"/redirect",
472+
fields={"target": "/headers_and_params", "status": "303 See Other"},
473+
)
474+
data = json.loads(response.data)
475+
assert data["params"] == {}
476+
assert "Content-Type" not in HTTPHeaderDict(data["headers"])
477+
467478
def test_bad_connect(self):
468479
with HTTPConnectionPool("badhost.invalid", self.port) as pool:
469480
with pytest.raises(MaxRetryError) as e:

test/with_dummyserver/test_poolmanager.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from dummyserver.server import HAS_IPV6
77
from dummyserver.testcase import HTTPDummyServerTestCase, IPv6HTTPDummyServerTestCase
8+
from urllib3._collections import HTTPHeaderDict
89
from urllib3.connectionpool import port_by_scheme
910
from urllib3.exceptions import MaxRetryError, URLSchemeUnknown
1011
from urllib3.poolmanager import PoolManager
@@ -236,6 +237,20 @@ def test_redirect_without_preload_releases_connection(self):
236237
assert r._pool.num_connections == 1
237238
assert len(http.pools) == 1
238239

240+
def test_303_redirect_makes_request_lose_body(self):
241+
with PoolManager() as http:
242+
response = http.request(
243+
"POST",
244+
"%s/redirect" % self.base_url,
245+
fields={
246+
"target": "%s/headers_and_params" % self.base_url,
247+
"status": "303 See Other",
248+
},
249+
)
250+
data = json.loads(response.data)
251+
assert data["params"] == {}
252+
assert "Content-Type" not in HTTPHeaderDict(data["headers"])
253+
239254
def test_unknown_scheme(self):
240255
with PoolManager() as http:
241256
unknown_scheme = "unknown"

0 commit comments

Comments
 (0)