From b5f0f43d0eab742216b4d73e426cbc82a30a449a Mon Sep 17 00:00:00 2001 From: joelazar Date: Tue, 10 Jun 2025 15:45:12 +0200 Subject: [PATCH 1/2] feat: redact passwords from error messages at database.Open --- database/error.go | 24 ++++++++++++++++++++++++ migrate.go | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/database/error.go b/database/error.go index eb802c753..ce18953ba 100644 --- a/database/error.go +++ b/database/error.go @@ -1,7 +1,9 @@ package database import ( + "errors" "fmt" + "regexp" ) // Error should be used for errors involving queries ran against the database @@ -25,3 +27,25 @@ func (e Error) Error() string { } return fmt.Sprintf("%v in line %v: %s (details: %v)", e.Err, e.Line, e.Query, e.OrigErr) } + +var ( + quotedKVRegex = regexp.MustCompile(`password='[^']*'`) + plainKVRegex = regexp.MustCompile(`password=[^ ]*`) + brokenURLRegex = regexp.MustCompile(`:[^:@]+?@`) +) + +func RedactPassword(err error) error { + input := err.Error() + + // Check if this error message contains password information + hasPassword := quotedKVRegex.MatchString(input) || plainKVRegex.MatchString(input) || brokenURLRegex.MatchString(input) + + if !hasPassword { + return err + } + input = quotedKVRegex.ReplaceAllLiteralString(input, "password=xxxxx") + input = plainKVRegex.ReplaceAllLiteralString(input, "password=xxxxx") + input = brokenURLRegex.ReplaceAllLiteralString(input, ":xxxxxx@") + + return errors.New(input) +} diff --git a/migrate.go b/migrate.go index 266cc04eb..d9acab89b 100644 --- a/migrate.go +++ b/migrate.go @@ -107,7 +107,7 @@ func New(sourceURL, databaseURL string) (*Migrate, error) { databaseDrv, err := database.Open(databaseURL) if err != nil { - return nil, fmt.Errorf("failed to open database: %w", err) + return nil, fmt.Errorf("failed to open database: %w", database.RedactPassword(err)) } m.databaseDrv = databaseDrv From bbb22bdf800a46172bcb5737f65ce7f8ea467652 Mon Sep 17 00:00:00 2001 From: joelazar Date: Tue, 10 Jun 2025 15:52:35 +0200 Subject: [PATCH 2/2] test: add tests for RedactPassword --- database/error_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 database/error_test.go diff --git a/database/error_test.go b/database/error_test.go new file mode 100644 index 000000000..6e224c5a9 --- /dev/null +++ b/database/error_test.go @@ -0,0 +1,71 @@ +package database + +import ( + "errors" + "testing" +) + +func TestRedactPassword(t *testing.T) { + testcases := []struct { + name string + input error + expected string + }{ + { + name: "quoted password in key-value format", + input: errors.New("connection failed: password='secret123' invalid"), + expected: "connection failed: password=xxxxx invalid", + }, + { + name: "plain password in key-value format", + input: errors.New("connection failed: password=secret123 invalid"), + expected: "connection failed: password=xxxxx invalid", + }, + { + name: "password in URL format", + input: errors.New("connection failed: postgres://user:secret123@localhost/db"), + expected: "connection failed: postgres://user:xxxxxx@localhost/db", + }, + { + name: "multiple password formats", + input: errors.New("connection failed: password='secret' and url postgres://user:pass@host"), + expected: "connection failed: password=xxxxx and url postgres://user:xxxxxx@host", + }, + { + name: "no password in error", + input: errors.New("connection failed: invalid host"), + expected: "connection failed: invalid host", + }, + { + name: "empty error", + input: errors.New(""), + expected: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + result := RedactPassword(tc.input) + if result.Error() != tc.expected { + t.Errorf("Expected %q, got %q", tc.expected, result.Error()) + } + }) + } +} + +type SpecialError struct { + msg string +} + +func (e SpecialError) Error() string { + return e.msg +} + +func TestRedactPasswordPreservesOriginalWhenNoPassword(t *testing.T) { + originalErr := SpecialError{msg: "no password here"} + result := RedactPassword(originalErr) + + if !errors.Is(result, originalErr) { + t.Error("Expected original error to be returned when no password found") + } +}