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 <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Feng Ruohang
2026-08-04 15:08:23 +08:00
parent 162ded3438
commit 0c14d81510
2 changed files with 43 additions and 5 deletions
+21 -5
View File
@@ -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, " ")
+22
View File
@@ -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)
}
}
}