diff --git a/internal/event/target/postgresql.go b/internal/event/target/postgresql.go index 9bd9a886f..228a2720c 100644 --- a/internal/event/target/postgresql.go +++ b/internal/event/target/postgresql.go @@ -403,25 +403,41 @@ func (target *PostgreSQLTarget) initPostgreSQL() error { return nil } +// quoteConnParam renders a value for a libpq keyword/value connection string. +// Parameters are separated by whitespace, so an unquoted value containing a +// space is read as the start of the next keyword and the connection fails on a +// string the operator never wrote. Single quotes make the value opaque; +// backslash and the quote itself are the only characters that need escaping +// inside them. +// +// Quoting unconditionally rather than only when required keeps the two cases +// from diverging: libpq treats 'localhost' and localhost identically, so there +// is nothing to gain by deciding per value which form to emit. +func quoteConnParam(v string) string { + return "'" + strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(v) + "'" +} + // NewPostgreSQLTarget - creates new PostgreSQL target. func NewPostgreSQLTarget(id string, args PostgreSQLArgs, loggerOnce logger.LogOnce) (*PostgreSQLTarget, error) { params := []string{args.ConnectionString} if args.ConnectionString == "" { params = []string{} if !args.Host.IsEmpty() { - params = append(params, "host="+args.Host.String()) + params = append(params, "host="+quoteConnParam(args.Host.String())) } if args.Port != "" { - params = append(params, "port="+args.Port) + params = append(params, "port="+quoteConnParam(args.Port)) } if args.Username != "" { - params = append(params, "username="+args.Username) + // libpq's keyword is "user"; "username" is rejected by the server + // as an unrecognized configuration parameter. + params = append(params, "user="+quoteConnParam(args.Username)) } if args.Password != "" { - params = append(params, "password="+args.Password) + params = append(params, "password="+quoteConnParam(args.Password)) } if args.Database != "" { - params = append(params, "dbname="+args.Database) + params = append(params, "dbname="+quoteConnParam(args.Database)) } } connStr := strings.Join(params, " ") diff --git a/internal/event/target/postgresql_test.go b/internal/event/target/postgresql_test.go index 9b5130e2e..bbafc4069 100644 --- a/internal/event/target/postgresql_test.go +++ b/internal/event/target/postgresql_test.go @@ -50,3 +50,25 @@ func TestPsqlTableNameValidation(t *testing.T) { } } } + +func TestQuoteConnParam(t *testing.T) { + testCases := []struct { + value string + expected string + }{ + {"localhost", "'localhost'"}, + {"5432", "'5432'"}, + // The reason this function exists: parameters are whitespace + // separated, so an unquoted space starts a new keyword. + {"pass word", "'pass word'"}, + {"it's", `'it\'s'`}, + {`back\slash`, `'back\\slash'`}, + {`'; host=evil.example.com; x='`, `'\'; host=evil.example.com; x=\''`}, + {"", "''"}, + } + for _, testCase := range testCases { + if got := quoteConnParam(testCase.value); got != testCase.expected { + t.Errorf("quoteConnParam(%q) = %s, expected %s", testCase.value, got, testCase.expected) + } + } +}