Skip to content

Commit f1fdbc3

Browse files
tianzhouclaude
andauthored
Reject sslrootcert and unknown sslmode values in PostgreSQL DSNs (#442)
On the DSN-only path (--dsn / DSN env), which bypasses TOML validation, the PostgreSQL parser silently dropped sslrootcert unless sslmode was verify-ca or verify-full. libpq treats require + a root CA as verify-ca, so a connection string copied from psql connected with no server verification and no warning. The same DSN inside a TOML source was already rejected. - sslrootcert with sslmode=require, disable or unset now fails with a message pointing at sslmode=verify-ca, matching TOML validation. - Unrecognised sslmode values (libpq's allow/prefer, typos) no longer fall through to `ssl: true`, which verified against the system CA store and never fell back to plaintext. They are rejected with the list of valid modes, as TOML already does. Replaces the two tests that asserted the silent-ignore behaviour and documents the difference from libpq in docs/config/command-line.mdx. Claude-Session: https://claude.ai/code/session_012UzToxWXzC7oQ9xMnhM192 Co-authored-by: Claude <noreply@anthropic.com>
1 parent f16dc5e commit f1fdbc3

3 files changed

Lines changed: 58 additions & 13 deletions

File tree

‎docs/config/command-line.mdx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ This page covers command-line flags and environment variables. For TOML configur
253253
| Parameter | Databases | Description | Example |
254254
|-----------|-----------|-------------|---------|
255255
| `sslmode` | PostgreSQL, MySQL, MariaDB, SQL Server, Oracle | SSL mode: `disable`, `require`, `verify-ca`, `verify-full` | `?sslmode=require` |
256-
| `sslrootcert` | PostgreSQL | CA certificate path (requires `verify-ca` or `verify-full`) | `?sslrootcert=~/.ssl/ca.pem` |
256+
| `sslrootcert` | PostgreSQL | CA certificate path (requires `verify-ca` or `verify-full`; rejected with `require`, `disable` or no `sslmode`) | `?sslrootcert=~/.ssl/ca.pem` |
257257
| `sslcert` | PostgreSQL | PEM client certificate for client certificate authentication (set with `sslkey`; requires `require`, `verify-ca` or `verify-full`) | `?sslcert=~/.ssl/client.crt` |
258258
| `sslkey` | PostgreSQL | Unencrypted PEM private key for `sslcert` | `?sslkey=~/.ssl/client.key` |
259259
| `instanceName` | SQL Server | Named instance | `?instanceName=SQLEXPRESS` |
@@ -277,6 +277,10 @@ This page covers command-line flags and environment variables. For TOML configur
277277
- `sslmode=verify-ca`: SSL with CA certificate verification, but no hostname check. **PostgreSQL only.** Use `sslrootcert` to specify the CA certificate path.
278278
- `sslmode=verify-full`: SSL with CA certificate and hostname verification. **PostgreSQL and Oracle.** On PostgreSQL use `sslrootcert` to specify the CA certificate path; on Oracle the server certificate is validated against the system trust store.
279279

280+
<Note>
281+
Unlike libpq, DBHub does not upgrade `sslmode=require` to `verify-ca` when `sslrootcert` is present: that combination is rejected at startup, so set `sslmode=verify-ca` explicitly. libpq's `allow` and `prefer` modes are not supported for PostgreSQL either; use `require` (or a verify mode) instead.
282+
</Note>
283+
280284
```bash
281285
# Examples
282286
postgres://user:password@localhost:5432/dbname?sslmode=disable

‎src/connectors/__tests__/dsn-parser.test.ts‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,35 @@ describe('DSN Parser - PostgreSQL SSL Modes', () => {
8787
await expect(parser.parse(dsn)).rejects.toThrow("Failed to read SSL root certificate at '/nonexistent/ca.pem'");
8888
});
8989

90-
it('should ignore sslrootcert when sslmode=require', async () => {
91-
const dsn = `postgres://user:pass@localhost:5432/db?sslmode=require&sslrootcert=${encodeURIComponent(certPath)}`;
92-
const config = await parser.parse(dsn);
93-
expect(config.ssl).toEqual({ rejectUnauthorized: false });
90+
it.each(['require', 'disable'])('should reject sslrootcert when sslmode=%s', async (sslmode) => {
91+
const dsn = `postgres://user:pass@localhost:5432/db?sslmode=${sslmode}&sslrootcert=${encodeURIComponent(certPath)}`;
92+
await expect(parser.parse(dsn)).rejects.toThrow(
93+
`sslrootcert requires sslmode 'verify-ca' or 'verify-full' (got '${sslmode}')`
94+
);
9495
});
9596

96-
it('should ignore sslrootcert when sslmode=disable', async () => {
97-
const dsn = `postgres://user:pass@localhost:5432/db?sslmode=disable&sslrootcert=${encodeURIComponent(certPath)}`;
98-
const config = await parser.parse(dsn);
99-
expect(config.ssl).toBe(false);
97+
it('should reject sslrootcert when sslmode is not set', async () => {
98+
const dsn = `postgres://user:pass@localhost:5432/db?sslrootcert=${encodeURIComponent(certPath)}`;
99+
await expect(parser.parse(dsn)).rejects.toThrow(
100+
"sslrootcert requires sslmode 'verify-ca' or 'verify-full' (got 'not set')"
101+
);
100102
});
103+
104+
it('should leave ssl unset when sslmode is not set', async () => {
105+
const config = await parser.parse('postgres://user:pass@localhost:5432/db');
106+
expect(config.ssl).toBeUndefined();
107+
});
108+
109+
it.each(['prefer', 'allow', 'verify_full', 'true'])(
110+
'should reject unsupported sslmode=%s',
111+
async (sslmode) => {
112+
await expect(
113+
parser.parse(`postgres://user:pass@localhost:5432/db?sslmode=${sslmode}`)
114+
).rejects.toThrow(
115+
`Unsupported sslmode '${sslmode}'. Valid values: disable, require, verify-ca, verify-full`
116+
);
117+
}
118+
);
101119
});
102120

103121
describe('DSN Parser - PostgreSQL client certificate (sslcert/sslkey)', () => {

‎src/connectors/postgres/index.ts‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ const POSTGRES_CLIENT_QUERY_TIMEOUT_GRACE_MS = 5_000;
3232
/** SSL modes in which a client certificate is presented to the server. */
3333
const CLIENT_CERT_SSL_MODES = ["require", "verify-ca", "verify-full"];
3434

35+
/** SSL modes this parser maps to a node-postgres `ssl` setting. */
36+
const SUPPORTED_SSL_MODES = ["disable", ...CLIENT_CERT_SSL_MODES];
37+
3538
/**
3639
* Read a PEM file referenced by an SSL DSN parameter, expanding a leading `~/`.
3740
* Wraps any read failure in FailedToReadCertificate so callers can tell a
@@ -67,10 +70,11 @@ function isEncryptedPemKey(pem: string): boolean {
6770
* - sslmode=require: SSL connection without certificate verification
6871
* - sslmode=verify-ca: SSL with CA certificate verification, no hostname check
6972
* - sslmode=verify-full: SSL with CA certificate and hostname verification
70-
* - Any other value: SSL with default Node.js TLS settings
73+
* - Any other value is rejected (libpq's allow/prefer have no node-postgres equivalent)
7174
*
7275
* Optional parameter for verify-ca/verify-full:
73-
* - sslrootcert=/path/to/ca.pem: Path to CA certificate bundle (supports ~/ expansion)
76+
* - sslrootcert=/path/to/ca.pem: Path to CA certificate bundle (supports ~/ expansion).
77+
* Rejected with any other sslmode rather than silently ignored.
7478
*
7579
* Optional parameters for client certificate authentication (require/verify-ca/verify-full):
7680
* - sslcert=/path/to/client.crt: PEM client certificate (supports ~/ expansion)
@@ -123,6 +127,27 @@ class PostgresDSNParser implements DSNParser {
123127
// Add other parameters as needed
124128
});
125129

130+
// An unrecognised mode (libpq's allow/prefer, or a typo) used to fall
131+
// through to `ssl: true`, which verifies against the system CA store and
132+
// never falls back to plaintext — the opposite of what allow/prefer ask
133+
// for. Reject it instead of guessing.
134+
if (sslmode !== undefined && !SUPPORTED_SSL_MODES.includes(sslmode)) {
135+
throw new Error(
136+
`Unsupported sslmode '${sslmode}'. Valid values: ${SUPPORTED_SSL_MODES.join(", ")}`
137+
);
138+
}
139+
140+
// sslrootcert is only read for verify-ca/verify-full. libpq treats
141+
// require + a root CA as verify-ca, so a DSN copied from psql would
142+
// otherwise connect unverified without any warning. Fail fast and let
143+
// the user pick the mode explicitly (matches TOML validation).
144+
if (sslrootcert !== undefined && sslmode !== "verify-ca" && sslmode !== "verify-full") {
145+
throw new FailedToReadCertificate(
146+
`sslrootcert requires sslmode 'verify-ca' or 'verify-full' (got '${sslmode ?? "not set"}'). ` +
147+
`Use sslmode=verify-ca to verify the server certificate against it, or remove sslrootcert.`
148+
);
149+
}
150+
126151
// Client certificate authentication: both halves are needed for a TLS
127152
// handshake, and node-postgres only speaks TLS in the modes below, so a
128153
// cert on a plaintext connection would be silently dropped. Fail fast.
@@ -154,8 +179,6 @@ class PostgresDSNParser implements DSNParser {
154179
sslConfig.ca = await readPemFile(sslrootcert, "SSL root certificate");
155180
}
156181
poolConfig.ssl = sslConfig;
157-
} else if (sslmode !== undefined) {
158-
poolConfig.ssl = true;
159182
}
160183

161184
if (sslcert !== undefined && sslkey !== undefined) {

0 commit comments

Comments
 (0)