From f27e874d554167e7bf0bead3d5b5bf8923abba05 Mon Sep 17 00:00:00 2001 From: Lukas Vogel Date: Fri, 12 Jun 2020 13:01:57 +0200 Subject: [PATCH] redact passwords in parse config errors Redact passwords when printing the parseConfigError in a best effort manner. This prevents people from leaking the password into logs, if they just print the error in logs. --- errors.go | 30 ++++++++++++++++++++++++++++-- errors_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ export_test.go | 11 +++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 errors_test.go create mode 100644 export_test.go diff --git a/errors.go b/errors.go index 7a21af98..b746c825 100644 --- a/errors.go +++ b/errors.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "net" + "net/url" + "regexp" "strings" errors "golang.org/x/xerrors" @@ -98,10 +100,11 @@ type parseConfigError struct { } func (e *parseConfigError) Error() string { + connString := redactPW(e.connString) if e.err == nil { - return fmt.Sprintf("cannot parse `%s`: %s", e.connString, e.msg) + return fmt.Sprintf("cannot parse `%s`: %s", connString, e.msg) } - return fmt.Sprintf("cannot parse `%s`: %s (%s)", e.connString, e.msg, e.err.Error()) + return fmt.Sprintf("cannot parse `%s`: %s (%s)", connString, e.msg, e.err.Error()) } func (e *parseConfigError) Unwrap() error { @@ -164,3 +167,26 @@ func (e *writeError) SafeToRetry() bool { func (e *writeError) Unwrap() error { return e.err } + +func redactPW(connString string) string { + if strings.HasPrefix(connString, "postgres://") || strings.HasPrefix(connString, "postgresql://") { + if u, err := url.Parse(connString); err == nil { + return redactURL(u) + } + } + quotedDSN := regexp.MustCompile(`password='[^']*'`) + connString = quotedDSN.ReplaceAllLiteralString(connString, "password=xxxxx") + plainDSN := regexp.MustCompile(`password=[^ ]*`) + connString = plainDSN.ReplaceAllLiteralString(connString, "password=xxxxx") + return connString +} + +func redactURL(u *url.URL) string { + if u == nil { + return "" + } + if _, pwSet := u.User.Password(); pwSet { + u.User = url.UserPassword(u.User.Username(), "xxxxx") + } + return u.String() +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 00000000..bef835f8 --- /dev/null +++ b/errors_test.go @@ -0,0 +1,44 @@ +package pgconn_test + +import ( + "testing" + + "github.com/jackc/pgconn" + "github.com/stretchr/testify/assert" +) + +func TestConfigError(t *testing.T) { + tests := []struct { + name string + err error + expectedMsg string + }{ + { + name: "url with password", + err: pgconn.NewParseConfigError("postgresql://foo:password@host", "msg", nil), + expectedMsg: "cannot parse `postgresql://foo:xxxxx@host`: msg", + }, + { + name: "dsn with password unquoted", + err: pgconn.NewParseConfigError("host=host password=password user=user", "msg", nil), + expectedMsg: "cannot parse `host=host password=xxxxx user=user`: msg", + }, + { + name: "dsn with password quoted", + err: pgconn.NewParseConfigError("host=host password='pass word' user=user", "msg", nil), + expectedMsg: "cannot parse `host=host password=xxxxx user=user`: msg", + }, + { + name: "weird url", + err: pgconn.NewParseConfigError("postgresql://foo::pasword@host:1:", "msg", nil), + expectedMsg: "cannot parse `postgresql://foo:xxxxx@host:1:`: msg", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.EqualError(t, tt.err, tt.expectedMsg) + }) + } +} diff --git a/export_test.go b/export_test.go new file mode 100644 index 00000000..2a0bad8b --- /dev/null +++ b/export_test.go @@ -0,0 +1,11 @@ +// File export_test exports some methods for better testing. + +package pgconn + +func NewParseConfigError(conn, msg string, err error) error { + return &parseConfigError{ + connString: conn, + msg: msg, + err: err, + } +}