mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-09-10 00:57:18 +00:00
fix(redactors): mask connection strings without a trailing database name (#2125)
* fix(redactors): mask connection strings without a trailing database name The built-in redactors that mask credentials in a database URI only match when a trailing /db-name is present, so `postgres://user:pass@host:5432` and `user:pass@tcp(host:3306)` were written to bundles unredacted. Generalize the URI credential redactor from http/https/ftp to any scheme, since it does not require a path, and add a companion redactor for the MySQL DSN form. The existing redactors that also mask the host and database name are unchanged, so URIs with a database name redact exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(redactors): mask slashes and colons in mysql DSN passwords The password in the no-database-name DSN redactor stopped at the first slash or colon, so `user:p/ass@tcp(host:3306)` was left unredacted. Run the password to the @ instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(redactors): mask mysql DSNs over tcp4, tcp6 and unix sockets Both DSN redactors only matched the tcp network type, so credentials in `user:pass@tcp6([::1]:3306)` and `user:pass@unix(/path/mysqld.sock)/db` were left unredacted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f4f02e0ee1
commit
ff9a7f87b8
+18
-5
@@ -312,20 +312,33 @@ func getRedactors(path string) ([]Redactor, error) {
|
||||
},
|
||||
name: "Redact values for environment variables with names beginning with 'user'",
|
||||
},
|
||||
// connection strings with username and password
|
||||
// credentials in a URI of any scheme, with or without a trailing path.
|
||||
// the fuller redactors below mask the host and database name as well,
|
||||
// but only match when a database name is present.
|
||||
// http://user:password@host:8888
|
||||
// postgres://user:password@host:5432
|
||||
{
|
||||
regex: LineRedactor{
|
||||
regex: `(?i)(https?|ftp)(:\/\/)(?P<mask>[^:\"\/]+){1}(:)(?P<mask>[^@\"\/]+){1}(?P<host>@[^:\/\s\"]+){1}(?P<port>:[\d]+)?`,
|
||||
scan: `https?|ftp`,
|
||||
regex: `(?i)([a-z][a-z\d+.\-]*)(:\/\/)(?P<mask>[^:\"\/]+){1}(:)(?P<mask>[^@\"\/]+){1}(?P<host>@[^:\/\s\"]+){1}(?P<port>:[\d]+)?`,
|
||||
scan: `:\/\/[^:\"\/]+:[^@\"\/]+@`,
|
||||
},
|
||||
name: "Redact connection strings with username and password",
|
||||
},
|
||||
// user:password@tcp(host:3309)/db-name
|
||||
{
|
||||
regex: LineRedactor{
|
||||
regex: `\b(?P<mask>[^:\"\/]*){1}(:)(?P<mask>[^:\"\/]*){1}(@tcp\()(?P<mask>[^:\"\/]*){1}(?P<port>:[\d]*)?(\)\/)(?P<mask>[\w\d\S-_]+){1}\b`,
|
||||
scan: `@tcp`,
|
||||
regex: `\b(?P<mask>[^:\"\/]*){1}(:)(?P<mask>[^:\"\/]*){1}(@(?:tcp[46]?|unix)\()(?P<mask>[^:\"\/]*){1}(?P<port>:[\d]*)?(\)\/)(?P<mask>[\w\d\S-_]+){1}\b`,
|
||||
scan: `@(?:tcp|unix)`,
|
||||
},
|
||||
name: "Redact database connection strings that contain username and password",
|
||||
},
|
||||
// user:password@tcp(host:3309), with no trailing /db-name. tcp4, tcp6 and
|
||||
// unix sockets are the other network types the mysql driver accepts.
|
||||
// the password runs to the @, so that slashes and colons in it are masked too.
|
||||
{
|
||||
regex: LineRedactor{
|
||||
regex: `(?P<mask>[^:\"\/\s]*){1}(:)(?P<mask>[^@\"\s]*){1}(@(?:tcp[46]?|unix)\()`,
|
||||
scan: `@(?:tcp|unix)`,
|
||||
},
|
||||
name: "Redact database connection strings that contain username and password",
|
||||
},
|
||||
|
||||
+149
-1
@@ -1720,7 +1720,7 @@ func Test_Redactors(t *testing.T) {
|
||||
}
|
||||
]`
|
||||
|
||||
wantRedactionsLen := 47
|
||||
wantRedactionsLen := 48
|
||||
wantRedactionsCount := 29
|
||||
|
||||
t.Run("test default redactors", func(t *testing.T) {
|
||||
@@ -1747,6 +1747,154 @@ func Test_Redactors(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// Connection strings are commonly written without a trailing database name, e.g.
|
||||
// "postgres://user:password@host:5432". The default redactors that mask the host and
|
||||
// database name only match when a database name is present, so credentials in these
|
||||
// URIs are masked by a separate redactor.
|
||||
func Test_DefaultRedactors_ConnectionStrings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "postgres uri with database name",
|
||||
input: "postgres://pg_user:pg_password@pg_host:5432/pg_database",
|
||||
want: "postgres://***HIDDEN***:***HIDDEN***@***HIDDEN***:5432/***HIDDEN***",
|
||||
},
|
||||
{
|
||||
name: "postgres uri without database name",
|
||||
input: "postgres://pg_user:pg_password@pg_host:5432",
|
||||
want: "postgres://***HIDDEN***:***HIDDEN***@pg_host:5432",
|
||||
},
|
||||
{
|
||||
name: "postgres uri without port or database name",
|
||||
input: "postgres://pg_user:pg_password@pg_host",
|
||||
want: "postgres://***HIDDEN***:***HIDDEN***@pg_host",
|
||||
},
|
||||
{
|
||||
name: "postgres uri with query parameters instead of a database name",
|
||||
input: "postgres://pg_user:pg_password@pg_host:5432?sslmode=require",
|
||||
want: "postgres://***HIDDEN***:***HIDDEN***@pg_host:5432?sslmode=require",
|
||||
},
|
||||
{
|
||||
name: "mysql uri without database name",
|
||||
input: "mysql://my_user:my_password@my_host:3306",
|
||||
want: "mysql://***HIDDEN***:***HIDDEN***@my_host:3306",
|
||||
},
|
||||
{
|
||||
name: "redis uri without database name",
|
||||
input: "redis://redis_user:redis_password@redis_host:6379",
|
||||
want: "redis://***HIDDEN***:***HIDDEN***@redis_host:6379",
|
||||
},
|
||||
{
|
||||
name: "amqp uri with a trailing slash and no database name",
|
||||
input: "amqp://rabbit_user:rabbit_password@rabbit_host:5672/",
|
||||
want: "amqp://***HIDDEN***:***HIDDEN***@rabbit_host:5672/",
|
||||
},
|
||||
{
|
||||
name: "uri without database name in json",
|
||||
input: `{"name":"DB_URI","value":"mongodb://mongo_user:mongo_password@mongo_host:27017"}`,
|
||||
want: `{"name":"DB_URI","value":"mongodb://***HIDDEN***:***HIDDEN***@mongo_host:27017"}`,
|
||||
},
|
||||
{
|
||||
name: "http url with credentials",
|
||||
input: "http://user:password@host:8888",
|
||||
want: "http://***HIDDEN***:***HIDDEN***@host:8888",
|
||||
},
|
||||
{
|
||||
name: "mysql dsn with database name",
|
||||
input: "dbuser:thisisasecret@tcp(dbserver.org:3309)/blog_production",
|
||||
want: "***HIDDEN***:***HIDDEN***@tcp(***HIDDEN***:3309)/***HIDDEN***",
|
||||
},
|
||||
{
|
||||
name: "mysql dsn without database name",
|
||||
input: "dbuser:thisisasecret@tcp(dbserver.org:3309)",
|
||||
want: "***HIDDEN***:***HIDDEN***@tcp(dbserver.org:3309)",
|
||||
},
|
||||
{
|
||||
name: "mysql dsn with a trailing slash and no database name",
|
||||
input: "dbuser:thisisasecret@tcp(dbserver.org:3309)/",
|
||||
want: "***HIDDEN***:***HIDDEN***@tcp(dbserver.org:3309)/",
|
||||
},
|
||||
{
|
||||
name: "mysql dsn over tcp6 with database name",
|
||||
input: "dbuser:thisisasecret@tcp6(dbserver.org:3306)/blog_production",
|
||||
want: "***HIDDEN***:***HIDDEN***@tcp6(***HIDDEN***:3306)/***HIDDEN***",
|
||||
},
|
||||
{
|
||||
name: "mysql dsn over tcp6 with an ipv6 address and no database name",
|
||||
input: "dbuser:thisisasecret@tcp6([::1]:3306)",
|
||||
want: "***HIDDEN***:***HIDDEN***@tcp6([::1]:3306)",
|
||||
},
|
||||
{
|
||||
name: "mysql dsn over tcp4 without database name",
|
||||
input: "dbuser:thisisasecret@tcp4(dbserver.org:3306)",
|
||||
want: "***HIDDEN***:***HIDDEN***@tcp4(dbserver.org:3306)",
|
||||
},
|
||||
{
|
||||
name: "mysql dsn over a unix socket",
|
||||
input: "dbuser:thisisasecret@unix(/var/run/mysqld/mysqld.sock)/blog_production",
|
||||
want: "***HIDDEN***:***HIDDEN***@unix(/var/run/mysqld/mysqld.sock)/blog_production",
|
||||
},
|
||||
{
|
||||
name: "mysql dsn with a slash in the password",
|
||||
input: "dbuser:this/is/a/secret@tcp(dbserver.org:3309)",
|
||||
want: "***HIDDEN***:***HIDDEN***@tcp(dbserver.org:3309)",
|
||||
},
|
||||
{
|
||||
name: "mysql dsn with a slash in the password and a database name",
|
||||
input: "dbuser:this/is/a/secret@tcp(dbserver.org:3309)/blog_production",
|
||||
want: "***HIDDEN***:***HIDDEN***@tcp(dbserver.org:3309)/blog_production",
|
||||
},
|
||||
{
|
||||
name: "mysql dsn in a log line",
|
||||
input: "INFO connecting to dbuser:thisisasecret@tcp(dbserver.org:3309)",
|
||||
want: "INFO connecting to ***HIDDEN***:***HIDDEN***@tcp(dbserver.org:3309)",
|
||||
},
|
||||
{
|
||||
name: "url without credentials is not redacted",
|
||||
input: "http://awesome-api:8013/graphql",
|
||||
want: "http://awesome-api:8013/graphql",
|
||||
},
|
||||
{
|
||||
name: "url without credentials or path is not redacted",
|
||||
input: "https://registry:10443",
|
||||
want: "https://registry:10443",
|
||||
},
|
||||
{
|
||||
name: "image reference is not redacted",
|
||||
input: "image: localhost:32000/awesome-api:e9a281f7@sha256:6e988461ffce2bac3561234f736b9a504bfda1911fa6432b90e6bbb16f67f925",
|
||||
want: "image: localhost:32000/awesome-api:e9a281f7@sha256:6e988461ffce2bac3561234f736b9a504bfda1911fa6432b90e6bbb16f67f925",
|
||||
},
|
||||
{
|
||||
name: "ssh remote is not redacted",
|
||||
input: "git@github.com:replicatedhq/troubleshoot.git",
|
||||
want: "git@github.com:replicatedhq/troubleshoot.git",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
ResetRedactionList()
|
||||
defer ResetRedactionList()
|
||||
|
||||
redactors, err := getRedactors("testpath")
|
||||
req.NoError(err)
|
||||
|
||||
nextReader := io.Reader(strings.NewReader(tt.input))
|
||||
for _, r := range redactors {
|
||||
nextReader = r.Redact(nextReader, "testpath")
|
||||
}
|
||||
|
||||
redacted, err := io.ReadAll(nextReader)
|
||||
req.NoError(err)
|
||||
req.Equal(tt.want, string(redacted))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_redactMatchesPath(t *testing.T) {
|
||||
type args struct {
|
||||
path string
|
||||
|
||||
@@ -131,7 +131,7 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Redact connection strings with username and password",
|
||||
re: `(?i)(https?|ftp)(:\/\/)(?P<mask>[^:\"\/]+){1}(:)(?P<mask>[^@\"\/]+){1}(?P<host>@[^:\/\s\"]+){1}(?P<port>:[\d]+)?`,
|
||||
re: `(?i)([a-z][a-z\d+.\-]*)(:\/\/)(?P<mask>[^:\"\/]+){1}(:)(?P<mask>[^@\"\/]+){1}(?P<host>@[^:\/\s\"]+){1}(?P<port>:[\d]+)?`,
|
||||
inputString: `http://user:password@host:8888`,
|
||||
wantString: "http://***HIDDEN***:***HIDDEN***@host:8888", // No trailing newline in input, so none in output
|
||||
wantRedactions: RedactionList{
|
||||
@@ -275,8 +275,8 @@ func TestNewSingleLineRedactor(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Redact connection strings With Scan",
|
||||
re: `(?i)(https?|ftp)(:\/\/)(?P<mask>[^:\"\/]+){1}(:)(?P<mask>[^@\"\/]+){1}(?P<host>@[^:\/\s\"]+){1}(?P<port>:[\d]+)?`,
|
||||
scan: `https?|ftp`,
|
||||
re: `(?i)([a-z][a-z\d+.\-]*)(:\/\/)(?P<mask>[^:\"\/]+){1}(:)(?P<mask>[^@\"\/]+){1}(?P<host>@[^:\/\s\"]+){1}(?P<port>:[\d]+)?`,
|
||||
scan: `:\/\/[^:\"\/]+:[^@\"\/]+@`,
|
||||
inputString: `http://user:password@host:8888;`,
|
||||
wantString: `http://***HIDDEN***:***HIDDEN***@host:8888;`, // No trailing newline in input, so none in output
|
||||
wantRedactions: RedactionList{
|
||||
|
||||
Reference in New Issue
Block a user