From 60d608ab64bf7551172587ce9ad43797a2049d69 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Tue, 23 Jun 2026 17:04:42 +0100 Subject: [PATCH] fix(pg): strip IPv6 URI brackets from host at connect/lookup, not in the parser --- packages/pg/lib/connection-parameters.js | 12 ++++++++--- packages/pg/lib/connection.js | 6 ++++++ packages/pg/lib/utils.js | 9 +++++++++ .../connection-parameters/creation-tests.js | 20 +++++++++++++++++++ .../pg/test/unit/connection/startup-tests.js | 12 +++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/pg/lib/connection-parameters.js b/packages/pg/lib/connection-parameters.js index 37987fd68..4593ada11 100644 --- a/packages/pg/lib/connection-parameters.js +++ b/packages/pg/lib/connection-parameters.js @@ -3,6 +3,7 @@ const dns = require('dns') const defaults = require('./defaults') +const { stripIpv6Brackets } = require('./utils') const parse = require('pg-connection-string').parse // parses a connection string @@ -164,8 +165,13 @@ class ConnectionParameters { if (this.replication) { params.push('replication=' + quoteParamValue(this.replication)) } - if (this.host) { - params.push('host=' + quoteParamValue(this.host)) + // Strip the brackets from an IPv6 URI literal ([::1] -> ::1) for the libpq + // host keyword and the dns.lookup below. libpq's host keyword does not accept + // the URI brackets, and dns.lookup('[::1]') fails with ENOTFOUND. this.host is + // left untouched. + const host = stripIpv6Brackets(this.host) + if (host) { + params.push('host=' + quoteParamValue(host)) } if (this.isDomainSocket) { return cb(null, params.join(' ')) @@ -173,7 +179,7 @@ class ConnectionParameters { if (this.client_encoding) { params.push('client_encoding=' + quoteParamValue(this.client_encoding)) } - dns.lookup(this.host, function (err, address) { + dns.lookup(host, function (err, address) { if (err) return cb(err, null) params.push('hostaddr=' + quoteParamValue(address)) return cb(null, params.join(' ')) diff --git a/packages/pg/lib/connection.js b/packages/pg/lib/connection.js index 63cc13a53..d26d0e81a 100644 --- a/packages/pg/lib/connection.js +++ b/packages/pg/lib/connection.js @@ -5,6 +5,7 @@ const EventEmitter = require('events').EventEmitter const { parse, serialize } = require('pg-protocol') const stream = require('./stream') const { getStream } = stream +const utils = require('./utils') const flushBuffer = serialize.flush() const syncBuffer = serialize.sync() @@ -39,6 +40,11 @@ class Connection extends EventEmitter { connect(port, host) { const self = this + // Strip the brackets from an IPv6 URI literal ([::1] -> ::1) so net.connect + // and the TLS servername (passed on to upgradeToSSL) get a bare address. This + // is local to the connection attempt; the parsed/user-supplied host is untouched. + host = utils.stripIpv6Brackets(host) + this._connecting = true this.stream.setNoDelay(true) this.stream.connect(port, host) diff --git a/packages/pg/lib/utils.js b/packages/pg/lib/utils.js index 638b43970..2c051a9c0 100644 --- a/packages/pg/lib/utils.js +++ b/packages/pg/lib/utils.js @@ -195,6 +195,14 @@ const escapeLiteral = function (str) { return escaped } +// Strip the square brackets that URI syntax wraps around an IPv6 literal +// (e.g. [::1] -> ::1). No-op for hostnames, IPv4, and domain-socket paths. +// libpq's host keyword, node's net.connect, and dns.lookup all want the bare +// address; the brackets are a URI delimiter, not part of the host. +function stripIpv6Brackets(host) { + return typeof host === 'string' ? host.replace(/^\[(.+)\]$/, '$1') : host +} + module.exports = { prepareValue: function prepareValueWrapper(value) { // this ensures that extra arguments do not get passed into prepareValue @@ -204,4 +212,5 @@ module.exports = { normalizeQueryConfig, escapeIdentifier, escapeLiteral, + stripIpv6Brackets, } diff --git a/packages/pg/test/unit/connection-parameters/creation-tests.js b/packages/pg/test/unit/connection-parameters/creation-tests.js index e326e2630..27b2d4c31 100644 --- a/packages/pg/test/unit/connection-parameters/creation-tests.js +++ b/packages/pg/test/unit/connection-parameters/creation-tests.js @@ -223,6 +223,26 @@ suite.test('error when dns fails', function () { ) }) +suite.test('strips IPv6 brackets from the libpq host and hostaddr', function () { + const config = { + user: 'brian', + host: '[::1]', + port: 5432, + } + const subject = new ConnectionParameters(config) + // the parsed host keeps the URI form; only the libpq string is normalized + assert.equal(subject.host, '[::1]') + return new Promise((resolve) => { + subject.getLibpqConnectionString(function (err, constring) { + assert(!err) + const parts = constring.split(' ') + checkForPart(parts, "host='::1'") + checkForPart(parts, "hostaddr='::1'") + resolve() + }) + }) +}) + suite.test('connecting to unix domain socket', function () { const config = { user: 'brian', diff --git a/packages/pg/test/unit/connection/startup-tests.js b/packages/pg/test/unit/connection/startup-tests.js index 65cc0c0aa..d08c63355 100644 --- a/packages/pg/test/unit/connection/startup-tests.js +++ b/packages/pg/test/unit/connection/startup-tests.js @@ -23,6 +23,18 @@ test('connection can take stream factory method', function () { assert.equal(con.stream, stream) }) +test('strips IPv6 brackets from the host before connecting', function () { + const stream = new MemoryStream() + let captured + stream.connect = function (port, host) { + captured = { port, host } + } + const con = new Connection({ stream: stream }) + con.connect(5432, '[::1]') + assert.equal(captured.host, '::1') + assert.equal(captured.port, 5432) +}) + test('using any stream', function () { const makeStream = function () { const stream = new MemoryStream()