diff --git a/packages/pg-connection-string/README.md b/packages/pg-connection-string/README.md index d3a16881a..26d52bff7 100644 --- a/packages/pg-connection-string/README.md +++ b/packages/pg-connection-string/README.md @@ -87,12 +87,22 @@ Query parameters follow a `?` character, including the following special query p * `host=` - sets `host` property, overriding the URL's host * `encoding=` - sets the `client_encoding` property * `ssl=1`, `ssl=true`, `ssl=0`, `ssl=false` - sets `ssl` to true or false, accordingly - * `sslmode=` + * `uselibpqcompat=true` - use libpq semantics + * `sslmode=` when `sslcompat` is not set * `sslmode=disable` - sets `ssl` to false * `sslmode=no-verify` - sets `ssl` to `{ rejectUnauthorized: false }` * `sslmode=prefer`, `sslmode=require`, `sslmode=verify-ca`, `sslmode=verify-full` - sets `ssl` to true + * `sslmode=` when `sslcompat=libpq` + * `sslmode=disable` - sets `ssl` to false + * `sslmode=prefer` - sets `ssl` to `{ rejectUnauthorized: false }` + * `sslmode=require` - sets `ssl` to `{ rejectUnauthorized: false }` unless `sslrootcert` is specified, in which case it behaves like `verify-ca` + * `sslmode=verify-ca` - sets `ssl` to `{ checkServerIdentity: no-op }` (verify CA, but not server identity). This verifies the presented certificate against the effective CA specified in sslrootcert. + * `sslmode=verify-full` - sets `ssl` to `{}` (verify CA and server identity) * `sslcert=` - reads data from the given file and includes the result as `ssl.cert` * `sslkey=` - reads data from the given file and includes the result as `ssl.key` * `sslrootcert=` - reads data from the given file and includes the result as `ssl.ca` A bare relative URL, such as `salesdata`, will indicate a database name while leaving other properties empty. + +> [!CAUTION] +> Choosing an sslmode other than verify-full has serious security implications. Please read https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS to understand the trade-offs. diff --git a/packages/pg-connection-string/index.d.ts b/packages/pg-connection-string/index.d.ts index d68cc23d3..88618ba92 100644 --- a/packages/pg-connection-string/index.d.ts +++ b/packages/pg-connection-string/index.d.ts @@ -1,6 +1,11 @@ import { ClientConfig } from 'pg' -export function parse(connectionString: string): ConnectionOptions +export function parse(connectionString: string, options: Options): ConnectionOptions + +export interface Options { + // Use libpq semantics when interpreting the connection string + useLibpqCompat?: boolean +} export interface ConnectionOptions { host: string | null diff --git a/packages/pg-connection-string/index.js b/packages/pg-connection-string/index.js index 53c3859e5..d40ec1252 100644 --- a/packages/pg-connection-string/index.js +++ b/packages/pg-connection-string/index.js @@ -5,7 +5,7 @@ //MIT License //parses a connection string -function parse(str) { +function parse(str, options = {}) { //unix socket if (str.charAt(0) === '/') { const config = str.split(' ') @@ -87,20 +87,58 @@ function parse(str) { config.ssl.ca = fs.readFileSync(config.sslrootcert).toString() } - switch (config.sslmode) { - case 'disable': { - config.ssl = false - break - } - case 'prefer': - case 'require': - case 'verify-ca': - case 'verify-full': { - break + if (options.useLibpqCompat && config.uselibpqcompat) { + throw new Error('Both useLibpqCompat and uselibpqcompat are set. Please use only one of them.') + } + + if (config.uselibpqcompat === 'true' || options.useLibpqCompat) { + switch (config.sslmode) { + case 'disable': { + config.ssl = false + break + } + case 'prefer': { + config.ssl.rejectUnauthorized = false + break + } + case 'require': { + if (config.sslrootcert) { + // If a root CA is specified, behavior of `sslmode=require` will be the same as that of `verify-ca` + config.ssl.checkServerIdentity = function () {} + } else { + config.ssl.rejectUnauthorized = false + } + break + } + case 'verify-ca': { + if (!config.ssl.ca) { + throw new Error( + 'SECURITY WARNING: Using sslmode=verify-ca requires specifying a CA with sslrootcert. If a public CA is used, verify-ca allows connections to a server that somebody else may have registered with the CA, making you vulnerable to Man-in-the-Middle attacks. Either specify a custom CA certificate with sslrootcert parameter or use sslmode=verify-full for proper security.' + ) + } + config.ssl.checkServerIdentity = function () {} + break + } + case 'verify-full': { + break + } } - case 'no-verify': { - config.ssl.rejectUnauthorized = false - break + } else { + switch (config.sslmode) { + case 'disable': { + config.ssl = false + break + } + case 'prefer': + case 'require': + case 'verify-ca': + case 'verify-full': { + break + } + case 'no-verify': { + config.ssl.rejectUnauthorized = false + break + } } } diff --git a/packages/pg-connection-string/test/parse.js b/packages/pg-connection-string/test/parse.js index 59f16a62e..12f64ab49 100644 --- a/packages/pg-connection-string/test/parse.js +++ b/packages/pg-connection-string/test/parse.js @@ -1,6 +1,7 @@ 'use strict' var chai = require('chai') +var expect = chai.expect chai.should() var parse = require('../').parse @@ -287,6 +288,114 @@ describe('parse', function () { }) }) + it('configuration parameter sslmode=disable with uselibpqcompat query param', function () { + var connectionString = 'pg:///?sslmode=disable&uselibpqcompat=true' + var subject = parse(connectionString) + subject.ssl.should.eql(false) + }) + + it('configuration parameter sslmode=prefer with uselibpqcompat query param', function () { + var connectionString = 'pg:///?sslmode=prefer&uselibpqcompat=true' + var subject = parse(connectionString) + subject.ssl.should.eql({ + rejectUnauthorized: false, + }) + }) + + it('configuration parameter sslmode=require with uselibpqcompat query param', function () { + var connectionString = 'pg:///?sslmode=require&uselibpqcompat=true' + var subject = parse(connectionString) + subject.ssl.should.eql({ + rejectUnauthorized: false, + }) + }) + + it('configuration parameter sslmode=verify-ca with uselibpqcompat query param', function () { + var connectionString = 'pg:///?sslmode=verify-ca&uselibpqcompat=true' + expect(function () { + parse(connectionString) + }).to.throw() + }) + + it('configuration parameter sslmode=verify-ca and sslrootcert with uselibpqcompat query param', function () { + var connectionString = 'pg:///?sslmode=verify-ca&uselibpqcompat=true&sslrootcert=' + __dirname + '/example.ca' + var subject = parse(connectionString) + subject.ssl.should.have.property('checkServerIdentity').that.is.a('function') + expect(subject.ssl.checkServerIdentity()).be.undefined + }) + + it('configuration parameter sslmode=verify-full with uselibpqcompat query param', function () { + var connectionString = 'pg:///?sslmode=verify-full&uselibpqcompat=true' + var subject = parse(connectionString) + subject.ssl.should.eql({}) + }) + + it('configuration parameter ssl=true and sslmode=require still work with sslrootcert=/path/to/ca with uselibpqcompat query param', function () { + var connectionString = + 'pg:///?ssl=true&sslrootcert=' + __dirname + '/example.ca&sslmode=require&uselibpqcompat=true' + var subject = parse(connectionString) + subject.ssl.should.have.property('ca', 'example ca\n') + subject.ssl.should.have.property('checkServerIdentity').that.is.a('function') + expect(subject.ssl.checkServerIdentity()).be.undefined + }) + + it('configuration parameter sslmode=disable with useLibpqCompat option', function () { + var connectionString = 'pg:///?sslmode=disable' + var subject = parse(connectionString, { useLibpqCompat: true }) + subject.ssl.should.eql(false) + }) + + it('configuration parameter sslmode=prefer with useLibpqCompat option', function () { + var connectionString = 'pg:///?sslmode=prefer' + var subject = parse(connectionString, { useLibpqCompat: true }) + subject.ssl.should.eql({ + rejectUnauthorized: false, + }) + }) + + it('configuration parameter sslmode=require with useLibpqCompat option', function () { + var connectionString = 'pg:///?sslmode=require' + var subject = parse(connectionString, { useLibpqCompat: true }) + subject.ssl.should.eql({ + rejectUnauthorized: false, + }) + }) + + it('configuration parameter sslmode=verify-ca with useLibpqCompat option', function () { + var connectionString = 'pg:///?sslmode=verify-ca' + expect(function () { + parse(connectionString, { useLibpqCompat: true }) + }).to.throw() + }) + + it('configuration parameter sslmode=verify-ca and sslrootcert with useLibpqCompat option', function () { + var connectionString = 'pg:///?sslmode=verify-ca&sslrootcert=' + __dirname + '/example.ca' + var subject = parse(connectionString, { useLibpqCompat: true }) + subject.ssl.should.have.property('checkServerIdentity').that.is.a('function') + expect(subject.ssl.checkServerIdentity()).be.undefined + }) + + it('configuration parameter sslmode=verify-full with useLibpqCompat option', function () { + var connectionString = 'pg:///?sslmode=verify-full' + var subject = parse(connectionString, { useLibpqCompat: true }) + subject.ssl.should.eql({}) + }) + + it('configuration parameter ssl=true and sslmode=require still work with sslrootcert=/path/to/ca with useLibpqCompat option', function () { + var connectionString = 'pg:///?ssl=true&sslrootcert=' + __dirname + '/example.ca&sslmode=require' + var subject = parse(connectionString, { useLibpqCompat: true }) + subject.ssl.should.have.property('ca', 'example ca\n') + subject.ssl.should.have.property('checkServerIdentity').that.is.a('function') + expect(subject.ssl.checkServerIdentity()).be.undefined + }) + + it('does not allow sslcompat query parameter and useLibpqCompat option at the same time', function () { + var connectionString = 'pg:///?uselibpqcompat=true' + expect(function () { + parse(connectionString, { useLibpqCompat: true }) + }).to.throw() + }) + it('allow other params like max, ...', function () { var subject = parse('pg://myhost/db?max=18&min=4') subject.max.should.equal('18')