Skip to content
Open
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/11410.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Strip authentication credentials from editable VCS URLs in ``pip freeze``.
23 changes: 22 additions & 1 deletion src/pip/_internal/operations/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

from pip._internal.exceptions import BadCommand, InstallationError
from pip._internal.metadata import BaseDistribution, get_environment
from pip._internal.models.direct_url import DirectUrl
from pip._internal.models.link import Link
from pip._internal.req.constructors import (
install_req_from_editable,
install_req_from_line,
Expand All @@ -27,6 +29,23 @@ class _EditableInfo(NamedTuple):
comments: list[str]


def _strip_auth_from_editable_requirement(req: str) -> str:
"""Remove credentials while preserving PEP 610-safe user information."""
link = Link(req)
if "@" not in link.netloc:
return req

# Apply the same auth rules used for PEP 610 direct URLs to the full
# editable requirement, preserving its VCS prefix, revision, and fragment.
safe_user_passwords = ("git",) if link.scheme.endswith("+ssh") else ()
sanitized_req = DirectUrl(url=req).to_dict(
strip_user_password=True,
safe_user_passwords=safe_user_passwords,
)["url"]
assert isinstance(sanitized_req, str)
return sanitized_req


def freeze(
requirement: list[str] | None = None,
local_only: bool = False,
Expand Down Expand Up @@ -214,7 +233,9 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
except InstallationError as exc:
logger.warning("Error when trying to get requirement for VCS system %s", exc)
else:
return _EditableInfo(requirement=req, comments=[])
return _EditableInfo(
requirement=_strip_auth_from_editable_requirement(req), comments=[]
)

logger.warning("Could not determine repository location of %s", location)

Expand Down
31 changes: 31 additions & 0 deletions tests/functional/test_freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,37 @@ def test_freeze_git_remote(script: PipTestEnvironment) -> None:
_check_output(result.stdout, expected)


@pytest.mark.git
def test_freeze_git_remote_strips_auth(script: PipTestEnvironment) -> None:
"""Test that freezing a Git clone does not expose remote credentials."""
pkg_version = _create_test_package(script.scratch_path)
script.run(
"git",
"clone",
os.fspath(pkg_version),
"pip-test-package",
expect_stderr=True,
)
repo_dir = script.scratch_path / "pip-test-package"
script.pip("install", "--no-build-isolation", "-e", repo_dir)
script.run(
"git",
"remote",
"set-url",
"origin",
"https://username:password@example.com/repo.git",
cwd=repo_dir,
)
revision = script.run("git", "rev-parse", "HEAD", cwd=repo_dir).stdout.strip()

result = script.pip("freeze", expect_stderr=True)

expected = f"...-e git+https://example.com/repo.git@{revision}#egg=version_pkg..."
_check_output(result.stdout, expected)
assert "username" not in result.stdout
assert "password" not in result.stdout


@need_mercurial
def test_freeze_mercurial_clone(script: PipTestEnvironment) -> None:
"""
Expand Down
64 changes: 64 additions & 0 deletions tests/unit/test_operations_freeze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import pytest

from pip._internal.operations.freeze import _strip_auth_from_editable_requirement


@pytest.mark.parametrize(
"requirement, expected",
[
(
"git+https://example.com/repo.git@rev#egg=project",
"git+https://example.com/repo.git@rev#egg=project",
),
(
"git+https://username:password@example.com/repo.git@rev#egg=project",
"git+https://example.com/repo.git@rev#egg=project",
),
(
"git+https://token@example.com/repo.git@rev#egg=project",
"git+https://example.com/repo.git@rev#egg=project",
),
(
"git+ssh://git@example.com/repo.git@rev#egg=project",
"git+ssh://git@example.com/repo.git@rev#egg=project",
),
(
"hg+ssh://git@example.com/repo@rev#egg=project",
"hg+ssh://git@example.com/repo@rev#egg=project",
),
(
"svn+ssh://git@example.com/repo@rev#egg=project",
"svn+ssh://git@example.com/repo@rev#egg=project",
),
(
"bzr+ssh://git@example.com/repo@rev#egg=project",
"bzr+ssh://git@example.com/repo@rev#egg=project",
),
(
"bzr+sftp://git@example.com/repo@rev#egg=project",
"bzr+sftp://example.com/repo@rev#egg=project",
),
(
"git+https://git@example.com/repo.git@rev#egg=project",
"git+https://example.com/repo.git@rev#egg=project",
),
(
"git+https://${TOKEN}@example.com/repo.git@rev#egg=project",
"git+https://${TOKEN}@example.com/repo.git@rev#egg=project",
),
(
"git+https://${USER}:${PASSWORD}@example.com/repo.git@rev#egg=project",
"git+https://${USER}:${PASSWORD}@example.com/repo.git@rev#egg=project",
),
(
"git+https://${USER}:password@example.com/repo.git@rev#egg=project",
"git+https://example.com/repo.git@rev#egg=project",
),
(
"git+ssh://%67%69%74@example.com/repo.git@rev#egg=project",
"git+ssh://example.com/repo.git@rev#egg=project",
),
],
)
def test_strip_auth_from_editable_requirement(requirement: str, expected: str) -> None:
assert _strip_auth_from_editable_requirement(requirement) == expected
Loading