Redact database URL passwords from logs

- Add SanitizeDatabaseURL() function to config package that redacts passwords from database connection URLs
- Handles various URL formats: postgresql, mysql, etc.
- Correctly handles passwords containing special characters including @ symbols by using LastIndex
- Update startup logging in cmd/server/main.go to use sanitized database URL
- Add comprehensive tests covering various URL formats and edge cases

This ensures sensitive database credentials are not exposed in application logs.
This commit is contained in:
2025-10-31 00:37:10 -07:00
parent 1cbe58888c
commit 5eaf6fe6fb
3 changed files with 108 additions and 1 deletions
+44
View File
@@ -418,6 +418,9 @@ func (c *Config) Sanitize() map[string]interface{} {
// Sanitize auth config
sanitized["auth"] = map[string]interface{}{
"enabled": c.Auth.Enabled,
"database": map[string]interface{}{
"url": SanitizeDatabaseURL(c.Auth.Database.URL),
},
"bootstrap": map[string]interface{}{
"enabled": c.Auth.Bootstrap.Enabled,
"admin_key_file": c.Auth.Bootstrap.AdminKeyFileName,
@@ -438,6 +441,47 @@ func (c *Config) Sanitize() map[string]interface{} {
return sanitized
}
// SanitizeDatabaseURL redacts the password from a database connection URL
// Handles formats like: postgresql://user:password@host:port/database
// Also handles passwords containing @ characters by finding the last @
func SanitizeDatabaseURL(dbURL string) string {
if dbURL == "" {
return ""
}
// Find the protocol (e.g., "postgresql://", "mysql://")
protocolIdx := strings.Index(dbURL, "://")
if protocolIdx == -1 {
return dbURL
}
protocol := dbURL[:protocolIdx+3]
remaining := dbURL[protocolIdx+3:]
// Find the LAST @ symbol that separates credentials from host
// (to handle passwords that may contain @ characters)
atIdx := strings.LastIndex(remaining, "@")
if atIdx == -1 {
// No credentials in the URL
return dbURL
}
// Extract credentials part and check if there's a password
credentials := remaining[:atIdx]
hostPart := remaining[atIdx:]
// Check if there's a colon (indicating a password)
colonIdx := strings.Index(credentials, ":")
if colonIdx == -1 {
// No password, just username
return protocol + credentials + hostPart
}
// Extract username and redact password
username := credentials[:colonIdx]
return protocol + username + ":***REDACTED***" + hostPart
}
// GetDefaultAccount returns the default account name for a notifier type, or the first account if no default is set
func (c *Config) GetDefaultAccount(notifierType domain.NotificationType) string {
switch notifierType {
+63
View File
@@ -0,0 +1,63 @@
package config
import (
"testing"
)
func TestSanitizeDatabaseURL(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "PostgreSQL with password",
input: "postgresql://user:password@localhost:5432/dbname",
expected: "postgresql://user:***REDACTED***@localhost:5432/dbname",
},
{
name: "PostgreSQL without password",
input: "postgresql://user@localhost:5432/dbname",
expected: "postgresql://user@localhost:5432/dbname",
},
{
name: "MySQL with special characters in password",
input: "mysql://root:SuperSecret123!@db.example.com:3306/mydb",
expected: "mysql://root:***REDACTED***@db.example.com:3306/mydb",
},
{
name: "Empty URL",
input: "",
expected: "",
},
{
name: "Invalid URL without protocol",
input: "invalid-url",
expected: "invalid-url",
},
{
name: "URL without credentials",
input: "postgresql://localhost:5432/dbname",
expected: "postgresql://localhost:5432/dbname",
},
{
name: "PostgreSQL with password containing colons",
input: "postgresql://user:pass:word@localhost:5432/dbname",
expected: "postgresql://user:***REDACTED***@localhost:5432/dbname",
},
{
name: "PostgreSQL with complex hostname and port",
input: "postgresql://admin:p@ssw0rd!@db-prod.example.com:5432/production",
expected: "postgresql://admin:***REDACTED***@db-prod.example.com:5432/production",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SanitizeDatabaseURL(tt.input)
if result != tt.expected {
t.Errorf("SanitizeDatabaseURL(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}