Redact quoted credentials in Git diagnostics

This commit is contained in:
2026-11-02 11:32:11 +00:00
parent 473670185c
commit 31f9e48219
3 changed files with 53 additions and 16 deletions

View File

@@ -6,7 +6,8 @@ import (
"strings"
)
var httpURLSpan = regexp.MustCompile(`(?i)https?://[^\s"'<>]+`)
// Match userinfo through @ before treating quotes as diagnostic delimiters.
var httpURLSpan = regexp.MustCompile(`(?i)https?://(?:[^\s/?#<>]*@)?[^\s"'<>]*`)
// DisplayURL hides HTTP userinfo without changing the endpoint used by Git.
func DisplayURL(raw string) string {

View File

@@ -12,6 +12,7 @@ func TestDisplayURL(t *testing.T) {
for _, test := range []struct{ input, want string }{
{"https://user:password@example.test/repo", "https://redacted@example.test/repo"},
{"https://token@example.test/repo", "https://redacted@example.test/repo"},
{"https://user:secret'part@example.test/repo", "https://redacted@example.test/repo"},
{"https://user%40example:secret%2Fvalue@example.test/repo", "https://redacted@example.test/repo"},
{"http://broken%zz:secret@example.test/repo", "[redacted URL]"},
{"https://example.test/repo", "https://example.test/repo"},
@@ -23,6 +24,32 @@ func TestDisplayURL(t *testing.T) {
}
}
func TestDisplayDiagnosticRedactsUserinfoBeforeQuotedContext(t *testing.T) {
for _, test := range []struct{ input, want string }{
{"fatal: 'https://user:secret'part@example.test/repo': denied", "fatal: 'https://redacted@example.test/repo': denied"},
{"failed at https://token'part@example.test", "failed at https://redacted@example.test"},
{"fatal: 'https://user:secret%27part@example.test/repo': denied", "fatal: 'https://redacted@example.test/repo': denied"},
{"fatal: 'https://broken%zz:secret'part@example.test/repo': denied", "fatal: '[redacted URL]': denied"},
{"fatal: 'https://token'part@': denied", "fatal: '[redacted URL]': denied"},
{"see 'https://example.test/path' and git@example.test:repo", "see 'https://example.test/path' and git@example.test:repo"},
} {
if got := displayDiagnostic(test.input); got != test.want {
t.Errorf("displayDiagnostic(%q) = %q, want %q", test.input, got, test.want)
}
}
}
func TestResolveRedactsCredentialsInNativeGitErrors(t *testing.T) {
fixture := newRemoteFixture(t)
for _, remote := range []string{"https://user:secret'part@example.test/repo", "https://user:secret%27part@example.test/repo"} {
_, err := Resolve(t.Context(), fixture.work, remote)
var exit *exec.ExitError
if !errors.As(err, &exit) || strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), "user:") || !strings.Contains(err.Error(), "example.test/repo") {
t.Fatalf("unsafe or incomplete native Git diagnostic: %v", err)
}
}
}
func TestGitFailuresRedactCredentialsAndPreserveCause(t *testing.T) {
installFakeGit(t, `case "$*" in
*check-ref-format*) exit 0 ;;
@@ -53,10 +80,12 @@ exit 23`)
}
func TestDisplayErrorPreservesWrappedCause(t *testing.T) {
cause := errors.New("failed at https://token@example.test/path")
err := displayError{cause: cause}
if !errors.Is(err, cause) || strings.Contains(err.Error(), "token") {
t.Fatalf("sanitized error = %v", err)
for _, userinfo := range []string{"token", "token'part", "token%27part"} {
cause := errors.New("failed at https://" + userinfo + "@example.test/path")
err := displayError{cause: cause}
if !errors.Is(err, cause) || strings.Contains(err.Error(), "token") {
t.Fatalf("sanitized error = %v", err)
}
}
}