diff --git a/readiness/checks.go b/readiness/checks.go index 77a6f86..ad907a4 100644 --- a/readiness/checks.go +++ b/readiness/checks.go @@ -182,10 +182,21 @@ var secretPatterns = []struct { {regexp.MustCompile(`(?i)(authorization:\s*basic\s+)\S+`), `${1}REDACTED`}, // Postgres / pq form: "password=abc123", "passwd=abc123", "pwd=abc123". - // Case-insensitive so "Password=" also redacts. - {regexp.MustCompile(`(?i)(password=)\S+`), `${1}REDACTED`}, - {regexp.MustCompile(`(?i)(passwd=)\S+`), `${1}REDACTED`}, - {regexp.MustCompile(`(?i)(pwd=)\S+`), `${1}REDACTED`}, + // Case-insensitive so "Password=" also redacts. Each pattern accepts + // three value shapes — pq's key-value DSN (https://pkg.go.dev/github.com/lib/pq) + // supports all of them so a customer connection-URL containing a space + // in the password lands in the error message in quoted form: + // bare: password=secret (ends at next whitespace) + // single-quoted: password='complex pass' (may contain spaces) + // double-quoted: password="complex pass" (may contain spaces) + // The plain `\S+` form caught only the bare case + the first non-space + // token of a quoted value, leaving the rest of the secret on the wire. + // 2026-05-31 fix — registry rows in checks_test.go::secretLeakCases + // (pq_*_quoted_*) lock the three shapes in CI; the registry-walk test + // fails closed on any regression to the bare `\S+` form. + {regexp.MustCompile(`(?i)(password=)(?:'[^']*'|"[^"]*"|\S+)`), `${1}REDACTED`}, + {regexp.MustCompile(`(?i)(passwd=)(?:'[^']*'|"[^"]*"|\S+)`), `${1}REDACTED`}, + {regexp.MustCompile(`(?i)(pwd=)(?:'[^']*'|"[^"]*"|\S+)`), `${1}REDACTED`}, // pq username leak: 'password authentication failed for user "instant"'. // Treat usernames as semi-sensitive — a leaked user name still gives diff --git a/readiness/checks_test.go b/readiness/checks_test.go index 308dbd9..fd15f0f 100644 --- a/readiness/checks_test.go +++ b/readiness/checks_test.go @@ -334,6 +334,16 @@ var secretLeakCases = []struct { {"pq_password_kv", `pq: FATAL: password=topsecret123 invalid`, []string{"topsecret123"}}, {"pq_passwd_kv", `pq: FATAL: passwd=topsecret123 invalid`, []string{"topsecret123"}}, {"pq_pwd_kv", `pq: FATAL: pwd=topsecret123 invalid`, []string{"topsecret123"}}, + // pq's key-value DSN supports single- and double-quoted password values + // when the secret contains whitespace. The bare `\S+` form caught only + // the first non-space token, leaving the rest of the secret on the wire. + // 2026-05-31 regression rows — each shape must not leak its content. + {"pq_password_single_quote", `pq: FATAL: password='top secret with space' invalid`, []string{"top secret with space", "secret with"}}, + {"pq_password_double_quote", `pq: FATAL: password="top secret with space" invalid`, []string{"top secret with space", "secret with"}}, + {"pq_passwd_single_quote", `pq: FATAL: passwd='multi word secret' invalid`, []string{"multi word secret", "word secret"}}, + {"pq_passwd_double_quote", `pq: FATAL: passwd="multi word secret" invalid`, []string{"multi word secret", "word secret"}}, + {"pq_pwd_single_quote", `pq: FATAL: pwd='abc 123 def' invalid`, []string{"abc 123 def", "123 def"}}, + {"pq_pwd_double_quote", `pq: FATAL: pwd="abc 123 def" invalid`, []string{"abc 123 def", "123 def"}}, {"pq_user_double_quote", `pq: password auth failed for user "dbadmin"`, []string{`"dbadmin"`}}, {"pq_user_single_quote", `pq: password auth failed for user 'dbadmin'`, []string{`'dbadmin'`}}, {"url_postgres", `dial postgres://app:p4ssw0rd@db:5432`, []string{"p4ssw0rd", "app:"}},