Redact quoted credentials in Git diagnostics
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user