Skip to content

Commit 4f0f54d

Browse files
authored
Merge pull request #2270 from yunaremaia/fix-redact-url-credentials
fix(util): redact URL credentials without corrupting the host
2 parents ae00cd2 + 0f89e36 commit 4f0f54d

2 files changed

Lines changed: 33 additions & 4 deletions

File tree

‎git/util.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -658,10 +658,11 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
658658
if url.password is None and url.username is None:
659659
continue
660660

661-
if url.password is not None:
662-
url = url._replace(netloc=url.netloc.replace(url.password, "*****"))
663-
if url.username is not None:
664-
url = url._replace(netloc=url.netloc.replace(url.username, "*****"))
661+
# Match urllib.parse's userinfo boundary. Keeping the raw hostinfo
662+
# preserves hostname case, IPv6 brackets, and port formatting.
663+
_, _, hostinfo = url.netloc.rpartition("@")
664+
redacted = "*****:*****" if url.password is not None else "*****"
665+
url = url._replace(netloc=f"{redacted}@{hostinfo}")
665666
new_cmdline[index] = urlunsplit(url)
666667
except ValueError:
667668
# This is not a valid URL.

‎test/test_util.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,34 @@ def test_remove_password_from_command_line(self):
828828
assert authorization not in " ".join(redacted_cmd_6)
829829
assert "http.extraHeader=Authorization: *****" in redacted_cmd_6
830830

831+
def test_remove_password_keeps_host_intact(self):
832+
"""Redaction must not touch the host, even when it contains the username."""
833+
redacted = remove_password_if_present(["git", "clone", "https://git@github.com/user/repo.git"])
834+
assert redacted == ["git", "clone", "https://*****@github.com/user/repo.git"]
835+
836+
redacted = remove_password_if_present(["git", "clone", "ssh://git@github.com/u/r.git"])
837+
assert redacted == ["git", "clone", "ssh://*****@github.com/u/r.git"]
838+
839+
def test_remove_empty_password_keeps_host_intact(self):
840+
"""An empty password must not expand into every position of the netloc."""
841+
redacted = remove_password_if_present(["git", "clone", "https://:@fakerepo.example.com/testrepo"])
842+
assert redacted == ["git", "clone", "https://*****:*****@fakerepo.example.com/testrepo"]
843+
844+
@ddt.data(
845+
(
846+
"https://user%40example.com:p%40ss@GitHub.COM:00443/repo@name?q=a@b#c@d",
847+
"https://*****:*****@GitHub.COM:00443/repo@name?q=a@b#c@d",
848+
),
849+
("//user:pass@[2001:db8::1]:0080/repo", "//*****:*****@[2001:db8::1]:0080/repo"),
850+
("https://user:p@ss@example.com/repo", "https://*****:*****@example.com/repo"),
851+
("https://user:@example.com/repo", "https://*****:*****@example.com/repo"),
852+
("https://@example.com/repo", "https://*****@example.com/repo"),
853+
("https://example.com/repo@name?q=a@b#c@d", "https://example.com/repo@name?q=a@b#c@d"),
854+
)
855+
@ddt.unpack
856+
def test_remove_password_preserves_url_components(self, url, expected):
857+
assert remove_password_if_present([url]) == [expected]
858+
831859

832860
def test_mode_str_to_int_accepts_bytes():
833861
assert mode_str_to_int("100644") == 0o100644

0 commit comments

Comments
 (0)