mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-21 10:13:01 +00:00
The startup line printed the DSN whole: * => goadmin:<password>@tcp(host:3306)/go-admin?... So every deployment wrote its own database credential into its own logs, where a log shipper, a support bundle or a screenshot of a terminal carries it onward. Found while reading deploy output, which is exactly how it leaks. The host and username stay - they are what makes the line worth printing - and only the password is replaced. Both DSN shapes this project accepts are covered, a sqlite path is left alone, and anything unparseable is withheld rather than echoed, since it may hold a credential too.
59 lines
1.9 KiB
Go
59 lines
1.9 KiB
Go
package database
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The startup line printed the DSN whole, so every deployment wrote its
|
|
// database password into its own logs - readable by anyone with the log file, a
|
|
// log shipper, or a screenshot of the terminal.
|
|
func TestRedactDSNKeepsThePasswordOut(t *testing.T) {
|
|
const secret = "s3cr3t-do-not-log"
|
|
|
|
cases := map[string]string{
|
|
"mysql": "goadmin:" + secret + "@tcp(db.example.com:3306)/go-admin?charset=utf8mb4&parseTime=True",
|
|
"postgres": "postgres://goadmin:" + secret + "@db.example.com:5432/go-admin?sslmode=disable",
|
|
"sqlserver": "sqlserver://goadmin:" + secret + "@db.example.com:1433?database=go-admin",
|
|
"empty-ish pwd": "goadmin:@tcp(db.example.com:3306)/go-admin",
|
|
}
|
|
|
|
for name, dsn := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
got := redactDSN(dsn)
|
|
if strings.Contains(got, secret) {
|
|
t.Fatalf("password survived redaction: %s", got)
|
|
}
|
|
// Still has to be useful: the host is what makes the line worth logging.
|
|
if !strings.Contains(got, "db.example.com") {
|
|
t.Errorf("host was lost, the line no longer says anything: %s", got)
|
|
}
|
|
if !strings.Contains(got, "goadmin") {
|
|
t.Errorf("username was lost: %s", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// sqlite has no credential to hide, and its path is the useful part.
|
|
func TestRedactDSNLeavesAPathAlone(t *testing.T) {
|
|
const path = "./go-admin-db.db"
|
|
if got := redactDSN(path); got != path {
|
|
t.Errorf("redactDSN(%q) = %q, want it unchanged", path, got)
|
|
}
|
|
}
|
|
|
|
// An unparseable string might still hold a password, so it is not echoed.
|
|
func TestRedactDSNSaysNothingAboutWhatItCannotParse(t *testing.T) {
|
|
got := redactDSN("://not a url at all:hunter2@")
|
|
if strings.Contains(got, "hunter2") {
|
|
t.Fatalf("password survived: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestRedactDSNHandlesEmpty(t *testing.T) {
|
|
if got := redactDSN(""); got != "" {
|
|
t.Errorf("redactDSN(\"\") = %q", got)
|
|
}
|
|
}
|