Skip to content

fix(pg-connection-string): get closer to libpq semantics for sslmode #2709

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
12 changes: 11 additions & 1 deletion packages/pg-connection-string/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,22 @@ Query parameters follow a `?` character, including the following special query p
* `host=<host>` - sets `host` property, overriding the URL's host
* `encoding=<encoding>` - sets the `client_encoding` property
* `ssl=1`, `ssl=true`, `ssl=0`, `ssl=false` - sets `ssl` to true or false, accordingly
* `sslmode=<sslmode>`
* `uselibpqcompat=true` - use libpq semantics
* `sslmode=<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=<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=<filename>` - reads data from the given file and includes the result as `ssl.cert`
* `sslkey=<filename>` - reads data from the given file and includes the result as `ssl.key`
* `sslrootcert=<filename>` - 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.
7 changes: 6 additions & 1 deletion packages/pg-connection-string/index.d.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
66 changes: 52 additions & 14 deletions packages/pg-connection-string/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(' ')
Expand Down Expand Up @@ -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
}
}
}

Expand Down
109 changes: 109 additions & 0 deletions packages/pg-connection-string/test/parse.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

var chai = require('chai')
var expect = chai.expect
chai.should()

var parse = require('../').parse
Expand Down Expand Up @@ -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')
Expand Down