From fb4de615e1765f9d8fe4b966350713e03e8694f5 Mon Sep 17 00:00:00 2001 From: martin-velay Date: Tue, 18 Aug 2026 13:54:11 +0200 Subject: [PATCH] fix: strip credentials from the recorded git origin url CI checkouts leave the access token in the remote url, like https://oauth2:@host/org/repo. We record that url in the run metadata and print it in the JSON and HTML reports, and those get archived, so the token leaks with them. ssh urls keep their user. git@ is part of the address, so dropping it would give a url that no longer reaches the remote. Passwords are stripped whatever the scheme. AI-assisted (Claude Code) - reviewed and approved by author Signed-off-by: martin-velay --- src/dvsim/utils/git.py | 29 +++++++++++++++++-- tests/utils/test_git.py | 64 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/dvsim/utils/git.py b/src/dvsim/utils/git.py index ce537bdf..0776d9de 100644 --- a/src/dvsim/utils/git.py +++ b/src/dvsim/utils/git.py @@ -5,6 +5,7 @@ """Git utility functions.""" from pathlib import Path +from urllib.parse import urlsplit, urlunsplit from git import Repo @@ -13,6 +14,26 @@ __all__ = ("repo_root",) +def strip_url_credentials(url: str) -> str: + """Remove any username/password from a URL, keeping the rest of it intact. + + A CI checkout leaves its access token in the remote URL as ``https://oauth2:@host/org/repo``. + That URL is recorded in the run metadata and published in the reports, which get archived, so the + token would outlive the run that leaked it. + + An ssh URL keeps its user: ``git@`` names an ssh account rather than an identity to authenticate + as, so dropping it gives a URL that no longer addresses the remote. A password is stripped + whatever the scheme carries it. + """ + parts = urlsplit(url) + userinfo, _, host = parts.netloc.rpartition("@") + if not userinfo: + return url + if ":" not in userinfo and parts.scheme not in ("http", "https"): + return url + return urlunsplit((parts.scheme, host, parts.path, parts.query, parts.fragment)) + + def repo_root(path: Path) -> Path | None: """Given a sub dir in a git repo provide the root path. @@ -56,7 +77,11 @@ def git_is_dirty(path: Path | None = None) -> bool: def git_origin_url(path: Path | None = None) -> str | None: - """Get the git remote origin url, or None if no ``origin`` remote is configured.""" + """Get the git remote origin url, or None if no ``origin`` remote is configured. + + Any credentials the remote carries are stripped, so that the url is safe to record in run + metadata and reports. + """ root = repo_root(path=path or Path.cwd()) if root is None: @@ -68,7 +93,7 @@ def git_origin_url(path: Path | None = None) -> str | None: if "origin" not in [remote.name for remote in r.remotes]: return None - return r.remote("origin").url + return strip_url_credentials(r.remote("origin").url) def git_https_url_with_commit(path: Path | None = None) -> str | None: diff --git a/tests/utils/test_git.py b/tests/utils/test_git.py index 06bde5ce..5516fe22 100644 --- a/tests/utils/test_git.py +++ b/tests/utils/test_git.py @@ -132,6 +132,66 @@ def test_git_origin_url(tmp_path: Path) -> None: equal_to(url), ) + @staticmethod + @pytest.mark.parametrize( + ("url", "expected"), + [ + ( + "https://github.com/lowRISC/test.git", + "https://github.com/lowRISC/test.git", + ), + ("git@github.com:lowRISC/test.git", "git@github.com:lowRISC/test.git"), + # An ssh user is part of the address rather than a credential, so it stays. Dropping + # it would leave a url that no longer reaches the remote. + ( + "ssh://git@github.com/lowRISC/test.git", + "ssh://git@github.com/lowRISC/test.git", + ), + ( + "git+ssh://git@github.com/lowRISC/test.git", + "git+ssh://git@github.com/lowRISC/test.git", + ), + ( + "https://oauth2:ghs_secrettoken@github.com/lowRISC/test.git", + "https://github.com/lowRISC/test.git", + ), + ( + "https://someuser@github.com/lowRISC/test.git", + "https://github.com/lowRISC/test.git", + ), + # A password is a credential whatever the scheme carries it. + ( + "ssh://user:secretpw@github.com/lowRISC/test.git", + "ssh://github.com/lowRISC/test.git", + ), + ], + ids=[ + "plain_https", + "ssh_scp_form", + "ssh_url_form", + "git_ssh_scheme", + "token", + "user_only", + "ssh_with_password", + ], + ) + def test_git_origin_url_strips_credentials(tmp_path: Path, url: str, expected: str) -> None: + """A token in the remote url never reaches the caller, whatever the url's shape. + + The url is recorded in the run metadata and published in the reports, so a credential + left in it outlives the run. Both ssh forms carry an '@' without being credentialed, and + have to survive untouched or the recorded url stops addressing the remote. + """ + r = Repo.init(path=tmp_path) + + file = tmp_path / "a" + file.write_text("file to commit") + r.index.add([file]) + r.index.commit("initial commit") + r.create_remote("origin", url) + + assert_that(git_origin_url(tmp_path), equal_to(expected)) + @staticmethod @pytest.mark.parametrize( ("url", "expected"), @@ -141,6 +201,10 @@ def test_git_origin_url(tmp_path: Path) -> None: "https://github.com/lowRISC/test.git", "https://github.com/lowRISC/test/tree/{commit}", ), + ( + "https://oauth2:ghs_secrettoken@github.com/lowRISC/test.git", + "https://github.com/lowRISC/test/tree/{commit}", + ), ], ) def test_git_https_url_with_commit(tmp_path: Path, url: str, expected: str) -> None: