From 0c14d81510fac0892d487dd96747cf6a49348f26 Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Tue, 4 Aug 2026 15:08:23 +0800 Subject: [PATCH] fix(notify): quote libpq connection parameters The discrete PostgreSQL connection path concatenated raw values into a libpq keyword/value string. Whitespace, quotes, or backslashes could split a value into additional parameters or make an otherwise valid configuration fail to parse; the path also used the unsupported keyword username instead of user. Render every generated value as a single-quoted libpq parameter, escape quotes and backslashes, and use the correct user key. Keep the existing connection_string form untouched. The earlier attempt to register migrated PostgreSQL and MySQL fields is deliberately absent because those key names collide with the legacy connection-string tokenizer. Focused tests cover ordinary values, whitespace, quotes, backslashes, and parameter-shaped input. Co-authored-by: ChatGPT Co-authored-by: Claude --- internal/event/target/postgresql.go | 26 +++++++++++++++++++----- internal/event/target/postgresql_test.go | 22 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) 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) + } + } +}