Skip to content

Commit 81ec063

Browse files
pmalouincharmanderhjr3
authored
feat(pg-connection-string): get closer to libpq semantics for sslmode
Allows user to change the semantics of `sslmode` to be as close as possible to libpq semantics. The opt in can be enabled using `useLibpqCompat` parsing option or the non-standard `uselibpqcompat` query string parameter. --------- Co-authored-by: Charmander <[email protected]> Co-authored-by: Herman J. Radtke III <[email protected]>
1 parent d8fb2f9 commit 81ec063

File tree

4 files changed

+178
-16
lines changed

4 files changed

+178
-16
lines changed

packages/pg-connection-string/README.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,22 @@ Query parameters follow a `?` character, including the following special query p
8787
* `host=<host>` - sets `host` property, overriding the URL's host
8888
* `encoding=<encoding>` - sets the `client_encoding` property
8989
* `ssl=1`, `ssl=true`, `ssl=0`, `ssl=false` - sets `ssl` to true or false, accordingly
90-
* `sslmode=<sslmode>`
90+
* `uselibpqcompat=true` - use libpq semantics
91+
* `sslmode=<sslmode>` when `sslcompat` is not set
9192
* `sslmode=disable` - sets `ssl` to false
9293
* `sslmode=no-verify` - sets `ssl` to `{ rejectUnauthorized: false }`
9394
* `sslmode=prefer`, `sslmode=require`, `sslmode=verify-ca`, `sslmode=verify-full` - sets `ssl` to true
95+
* `sslmode=<sslmode>` when `sslcompat=libpq`
96+
* `sslmode=disable` - sets `ssl` to false
97+
* `sslmode=prefer` - sets `ssl` to `{ rejectUnauthorized: false }`
98+
* `sslmode=require` - sets `ssl` to `{ rejectUnauthorized: false }` unless `sslrootcert` is specified, in which case it behaves like `verify-ca`
99+
* `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.
100+
* `sslmode=verify-full` - sets `ssl` to `{}` (verify CA and server identity)
94101
* `sslcert=<filename>` - reads data from the given file and includes the result as `ssl.cert`
95102
* `sslkey=<filename>` - reads data from the given file and includes the result as `ssl.key`
96103
* `sslrootcert=<filename>` - reads data from the given file and includes the result as `ssl.ca`
97104

98105
A bare relative URL, such as `salesdata`, will indicate a database name while leaving other properties empty.
106+
107+
> [!CAUTION]
108+
> 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.

packages/pg-connection-string/index.d.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { ClientConfig } from 'pg'
22

3-
export function parse(connectionString: string): ConnectionOptions
3+
export function parse(connectionString: string, options: Options): ConnectionOptions
4+
5+
export interface Options {
6+
// Use libpq semantics when interpreting the connection string
7+
useLibpqCompat?: boolean
8+
}
49

510
export interface ConnectionOptions {
611
host: string | null

packages/pg-connection-string/index.js

+52-14
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//MIT License
66

77
//parses a connection string
8-
function parse(str) {
8+
function parse(str, options = {}) {
99
//unix socket
1010
if (str.charAt(0) === '/') {
1111
const config = str.split(' ')
@@ -87,20 +87,58 @@ function parse(str) {
8787
config.ssl.ca = fs.readFileSync(config.sslrootcert).toString()
8888
}
8989

90-
switch (config.sslmode) {
91-
case 'disable': {
92-
config.ssl = false
93-
break
94-
}
95-
case 'prefer':
96-
case 'require':
97-
case 'verify-ca':
98-
case 'verify-full': {
99-
break
90+
if (options.useLibpqCompat && config.uselibpqcompat) {
91+
throw new Error('Both useLibpqCompat and uselibpqcompat are set. Please use only one of them.')
92+
}
93+
94+
if (config.uselibpqcompat === 'true' || options.useLibpqCompat) {
95+
switch (config.sslmode) {
96+
case 'disable': {
97+
config.ssl = false
98+
break
99+
}
100+
case 'prefer': {
101+
config.ssl.rejectUnauthorized = false
102+
break
103+
}
104+
case 'require': {
105+
if (config.sslrootcert) {
106+
// If a root CA is specified, behavior of `sslmode=require` will be the same as that of `verify-ca`
107+
config.ssl.checkServerIdentity = function () {}
108+
} else {
109+
config.ssl.rejectUnauthorized = false
110+
}
111+
break
112+
}
113+
case 'verify-ca': {
114+
if (!config.ssl.ca) {
115+
throw new Error(
116+
'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.'
117+
)
118+
}
119+
config.ssl.checkServerIdentity = function () {}
120+
break
121+
}
122+
case 'verify-full': {
123+
break
124+
}
100125
}
101-
case 'no-verify': {
102-
config.ssl.rejectUnauthorized = false
103-
break
126+
} else {
127+
switch (config.sslmode) {
128+
case 'disable': {
129+
config.ssl = false
130+
break
131+
}
132+
case 'prefer':
133+
case 'require':
134+
case 'verify-ca':
135+
case 'verify-full': {
136+
break
137+
}
138+
case 'no-verify': {
139+
config.ssl.rejectUnauthorized = false
140+
break
141+
}
104142
}
105143
}
106144

packages/pg-connection-string/test/parse.js

+109
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
var chai = require('chai')
4+
var expect = chai.expect
45
chai.should()
56

67
var parse = require('../').parse
@@ -287,6 +288,114 @@ describe('parse', function () {
287288
})
288289
})
289290

291+
it('configuration parameter sslmode=disable with uselibpqcompat query param', function () {
292+
var connectionString = 'pg:///?sslmode=disable&uselibpqcompat=true'
293+
var subject = parse(connectionString)
294+
subject.ssl.should.eql(false)
295+
})
296+
297+
it('configuration parameter sslmode=prefer with uselibpqcompat query param', function () {
298+
var connectionString = 'pg:///?sslmode=prefer&uselibpqcompat=true'
299+
var subject = parse(connectionString)
300+
subject.ssl.should.eql({
301+
rejectUnauthorized: false,
302+
})
303+
})
304+
305+
it('configuration parameter sslmode=require with uselibpqcompat query param', function () {
306+
var connectionString = 'pg:///?sslmode=require&uselibpqcompat=true'
307+
var subject = parse(connectionString)
308+
subject.ssl.should.eql({
309+
rejectUnauthorized: false,
310+
})
311+
})
312+
313+
it('configuration parameter sslmode=verify-ca with uselibpqcompat query param', function () {
314+
var connectionString = 'pg:///?sslmode=verify-ca&uselibpqcompat=true'
315+
expect(function () {
316+
parse(connectionString)
317+
}).to.throw()
318+
})
319+
320+
it('configuration parameter sslmode=verify-ca and sslrootcert with uselibpqcompat query param', function () {
321+
var connectionString = 'pg:///?sslmode=verify-ca&uselibpqcompat=true&sslrootcert=' + __dirname + '/example.ca'
322+
var subject = parse(connectionString)
323+
subject.ssl.should.have.property('checkServerIdentity').that.is.a('function')
324+
expect(subject.ssl.checkServerIdentity()).be.undefined
325+
})
326+
327+
it('configuration parameter sslmode=verify-full with uselibpqcompat query param', function () {
328+
var connectionString = 'pg:///?sslmode=verify-full&uselibpqcompat=true'
329+
var subject = parse(connectionString)
330+
subject.ssl.should.eql({})
331+
})
332+
333+
it('configuration parameter ssl=true and sslmode=require still work with sslrootcert=/path/to/ca with uselibpqcompat query param', function () {
334+
var connectionString =
335+
'pg:///?ssl=true&sslrootcert=' + __dirname + '/example.ca&sslmode=require&uselibpqcompat=true'
336+
var subject = parse(connectionString)
337+
subject.ssl.should.have.property('ca', 'example ca\n')
338+
subject.ssl.should.have.property('checkServerIdentity').that.is.a('function')
339+
expect(subject.ssl.checkServerIdentity()).be.undefined
340+
})
341+
342+
it('configuration parameter sslmode=disable with useLibpqCompat option', function () {
343+
var connectionString = 'pg:///?sslmode=disable'
344+
var subject = parse(connectionString, { useLibpqCompat: true })
345+
subject.ssl.should.eql(false)
346+
})
347+
348+
it('configuration parameter sslmode=prefer with useLibpqCompat option', function () {
349+
var connectionString = 'pg:///?sslmode=prefer'
350+
var subject = parse(connectionString, { useLibpqCompat: true })
351+
subject.ssl.should.eql({
352+
rejectUnauthorized: false,
353+
})
354+
})
355+
356+
it('configuration parameter sslmode=require with useLibpqCompat option', function () {
357+
var connectionString = 'pg:///?sslmode=require'
358+
var subject = parse(connectionString, { useLibpqCompat: true })
359+
subject.ssl.should.eql({
360+
rejectUnauthorized: false,
361+
})
362+
})
363+
364+
it('configuration parameter sslmode=verify-ca with useLibpqCompat option', function () {
365+
var connectionString = 'pg:///?sslmode=verify-ca'
366+
expect(function () {
367+
parse(connectionString, { useLibpqCompat: true })
368+
}).to.throw()
369+
})
370+
371+
it('configuration parameter sslmode=verify-ca and sslrootcert with useLibpqCompat option', function () {
372+
var connectionString = 'pg:///?sslmode=verify-ca&sslrootcert=' + __dirname + '/example.ca'
373+
var subject = parse(connectionString, { useLibpqCompat: true })
374+
subject.ssl.should.have.property('checkServerIdentity').that.is.a('function')
375+
expect(subject.ssl.checkServerIdentity()).be.undefined
376+
})
377+
378+
it('configuration parameter sslmode=verify-full with useLibpqCompat option', function () {
379+
var connectionString = 'pg:///?sslmode=verify-full'
380+
var subject = parse(connectionString, { useLibpqCompat: true })
381+
subject.ssl.should.eql({})
382+
})
383+
384+
it('configuration parameter ssl=true and sslmode=require still work with sslrootcert=/path/to/ca with useLibpqCompat option', function () {
385+
var connectionString = 'pg:///?ssl=true&sslrootcert=' + __dirname + '/example.ca&sslmode=require'
386+
var subject = parse(connectionString, { useLibpqCompat: true })
387+
subject.ssl.should.have.property('ca', 'example ca\n')
388+
subject.ssl.should.have.property('checkServerIdentity').that.is.a('function')
389+
expect(subject.ssl.checkServerIdentity()).be.undefined
390+
})
391+
392+
it('does not allow sslcompat query parameter and useLibpqCompat option at the same time', function () {
393+
var connectionString = 'pg:///?uselibpqcompat=true'
394+
expect(function () {
395+
parse(connectionString, { useLibpqCompat: true })
396+
}).to.throw()
397+
})
398+
290399
it('allow other params like max, ...', function () {
291400
var subject = parse('pg://myhost/db?max=18&min=4')
292401
subject.max.should.equal('18')

0 commit comments

Comments
 (0)