Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/14171.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Refuse to follow an HTTP redirect to a ``file:`` URL.
17 changes: 17 additions & 0 deletions src/pip/_internal/network/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,23 @@ def is_secure_origin(self, location: Link) -> bool:

return False

def get_redirect_target(self, resp: Response) -> str | None:
target = super().get_redirect_target(resp)
if target is None:
return None
# Redirecting to a file:// URL doesn't make much sense and
# shouldn't be allowed.
scheme = urllib.parse.urlparse(target).scheme
if scheme and scheme.lower() not in ("http", "https"):
logger.warning(
"Not following redirect from %s to %s: a redirect to a non-http(s)"
" location is not allowed.",
redact_auth_from_url(resp.url),
redact_auth_from_url(target),
)
return None
return target

def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> Response: # type: ignore[override]
# Allow setting a default timeout on a session
kwargs.setdefault("timeout", self.timeout)
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/test_network_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
user_agent,
)
from pip._internal.utils.misc import CI_ENVIRONMENT_VARIABLES
from pip._internal.utils.urls import path_to_url

from tests.lib.output import render_to_text
from tests.lib.server import make_mock_server, server_running
Expand Down Expand Up @@ -461,6 +462,49 @@ def test_unset_proxy_is_distinct_from_empty(
assert self._resolved_proxy(session, "http://example.com") is not None


class TestRedirectScheme:
@staticmethod
def _make_redirect_response(location: str) -> requests.Response:
resp = requests.Response()
resp.status_code = 302
resp.headers["Location"] = location
resp.url = "https://example.com/simple/foo/"
request = requests.PreparedRequest()
request.prepare(method="GET", url=resp.url, headers={})
resp.request = request
return resp

def test_get_redirect_target_refuses_file_scheme(
self, tmpdir: Path, caplog: pytest.LogCaptureFixture
) -> None:
# A remote server must not be able to redirect pip into the file:// adapter
# and have it read a local file.
secret = tmpdir.joinpath("secret.txt")
secret.write_text("s3cr3t", encoding="utf-8")
resp = self._make_redirect_response(path_to_url(str(secret)))

session = PipSession()
with caplog.at_level(logging.WARNING):
assert session.get_redirect_target(resp) is None
assert "non-http(s) location is not allowed" in caplog.text

# Following the redirect the way requests does must not read the file.
followed = list(session.resolve_redirects(resp, resp.request))
assert followed == []

@pytest.mark.parametrize(
"location",
[
"https://other.example.com/elsewhere/",
"http://other.example.com/elsewhere/",
"/relative/path/",
],
)
def test_get_redirect_target_allows_http_schemes(self, location: str) -> None:
resp = self._make_redirect_response(location)
assert PipSession().get_redirect_target(resp) == location


class TestSSLContextAdapterMixinProxy:
"""Regression tests for https://github.com/pypa/pip/issues/13465

Expand Down
Loading