diff --git a/git/util.py b/git/util.py index 8584b0e7f..e81ec0237 100644 --- a/git/util.py +++ b/git/util.py @@ -540,8 +540,7 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]: - """Parse any command line argument and if one of the elements is an URL with a - username and/or password, replace them by stars (in-place). + """Redact credentials in URLs and HTTP Authorization extra headers in a command line. If nothing is found, this just returns the command line as-is. @@ -551,6 +550,16 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]: new_cmdline = [] for index, to_parse in enumerate(cmdline): new_cmdline.append(to_parse) + config_key, separator, header = to_parse.partition("=") + header_name, colon, _ = header.partition(":") + if ( + separator + and colon + and config_key.lower().endswith(".extraheader") + and header_name.strip().lower() == "authorization" + ): + new_cmdline[index] = "%s%s%s%s *****" % (config_key, separator, header_name, colon) + continue try: url = urlsplit(to_parse) # Remove password from the URL if present. diff --git a/test/test_git.py b/test/test_git.py index a6698a021..09a92a026 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -395,6 +395,24 @@ def test_it_raises_proper_exception_with_output_stream(self): with self.assertRaises(GitCommandError): self.git.checkout("non-existent-branch", output_stream=tmp_file) + def test_it_redacts_authorization_extra_header_from_error(self): + token = "fake-token-1234" + command = [ + "git", + "-c", + "http.extraHeader=Authorization: Bearer %s" % token, + "rev-parse", + "--verify", + "refs/does-not-exist", + ] + + with self.assertRaises(GitCommandError) as context: + self.git.execute(command) + + message = str(context.exception) + self.assertNotIn(token, message) + self.assertIn("http.extraHeader=Authorization: *****", message) + def test_it_accepts_environment_variables(self): filename = fixture_path("ls_tree_empty") with open(filename, "r") as fh: diff --git a/test/test_util.py b/test/test_util.py index e7453769a..4bd1eebca 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -671,6 +671,7 @@ def test_pickle_tzoffset(self): def test_remove_password_from_command_line(self): username = "fakeuser" password = "fakepassword1234" + authorization = "Bearer fake-token-1234" url_with_user_and_pass = "https://{}:{}@fakerepo.example.com/testrepo".format(username, password) url_with_user = "https://{}@fakerepo.example.com/testrepo".format(username) url_with_pass = "https://:{}@fakerepo.example.com/testrepo".format(password) @@ -681,6 +682,7 @@ def test_remove_password_from_command_line(self): cmd_3 = ["git", "clone", "-v", url_with_pass] cmd_4 = ["git", "clone", "-v", url_without_user_or_pass] cmd_5 = ["no", "url", "in", "this", "one"] + cmd_6 = ["git", "-c", "http.extraHeader=Authorization: %s" % authorization, "fetch"] redacted_cmd_1 = remove_password_if_present(cmd_1) assert username not in " ".join(redacted_cmd_1) @@ -700,3 +702,7 @@ def test_remove_password_from_command_line(self): assert cmd_4 == remove_password_if_present(cmd_4) assert cmd_5 == remove_password_if_present(cmd_5) + + redacted_cmd_6 = remove_password_if_present(cmd_6) + assert authorization not in " ".join(redacted_cmd_6) + assert "http.extraHeader=Authorization: *****" in redacted_cmd_6