Skip to content

Validate sslmode and gssencmode at parse time and in the constructor - #47

Open
PingoLee wants to merge 1 commit into
JuliaDatabases:mainfrom
PingoLee:early-mode-validation
Open

PingoLee wants to merge 1 commit into
JuliaDatabases:mainfrom
PingoLee:early-mode-validation

Conversation

@PingoLee

@PingoLee PingoLee commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

An invalid sslmode or gssencmode passes parse_dsn and is only rejected when connecting. The sslmode check also runs late: it is in _connect_tls!, after the TCP connect, and when GSS encryption is negotiated API.connect returns before reaching it. So gssencmode=require sslmode=bogus connects on a machine with a Kerberos ticket and fails on one without (#28, item 3).

This validates both values before connecting, in one place:

  • The accepted values are two constant tuples, Postgres.SSLMODES (disable, prefer, require, verify-full) and Postgres.GSSENCMODES (disable, prefer, require). The parser, the constructors, and the connect and cancel paths all read them, and the error message is built from them.
  • parse_dsn and validate_dsn check them after environment defaults are applied, so PGSSLMODE and PGGSSENCMODE are covered. The check runs where Add validate_dsn for structured connection-parameter errors #33 already rejected malformed UTF-8, between connect_timeout and gssdelegation, so the order in which errors are reported does not change otherwise. The message is the one Add validate_dsn for structured connection-parameter errors #33 introduced: invalid value "verfy-full" for connection parameter "sslmode"; expected one of disable, prefer, require, verify-full. validate_dsn reports key = "sslmode" or "gssencmode" and reason = :invalid_value.
  • The ConnectionParams and Connection keyword constructors check them too, as they already check numeric_overflow, and throw an ArgumentError with the same message. A keyword caller never goes through the parser.
  • API.connect checks both at the top, before any dial, so no negotiation path skips the sslmode check. These checks stay Postgres.Errors as before. The constructors make them unreachable, so they are a backstop for direct callers. cancel_request gets the same backstop, since an unknown mode there would take the branches that may send the cancel key in cleartext.

Values stay case-insensitive (Verify-Full is accepted), no value is added, and the constructors store the value as given, as before.

Behavior change. Inputs that connect today are unaffected, apart from case 3 below. Configurations that would fail to connect now fail earlier, with an ArgumentError instead of a Postgres.Error (direct API.connect and API.cancel_request callers still get the Postgres.Error):

  1. A caller that only parses, for example to check a config without connecting, gets an ArgumentError from parse_dsn where it got a ConnectionParams, and an error from validate_dsn where it got nothing. This also applies when the value comes from PGSSLMODE or PGGSSENCMODE, and to an empty value such as an unexpanded ${PGSSLMODE}. The same applies to ConnectionParams(; sslmode = …).
  2. A caller that caught Postgres.Error for invalid sslmode / invalid gssencmode at connect time now gets an ArgumentError, earlier, from parse_dsn or from the constructor. A keyword value with malformed UTF-8 now also gets this ArgumentError, where lowercase used to throw InvalidCharError while connecting.
  3. gssencmode=require (or prefer) with an invalid sslmode no longer connects over GSS encryption.

I'd suggest a minor-version bump and a release note for this. Adds the two constants (not exported or public). Happy to mark them public if that is useful to callers. No runtime dependency changes.

Closes #28.

Validation:

  • Accepted values: every accepted value, as lowercase, UPPER, and Title case, is accepted through keyword DSNs, URIs, PGSSLMODE/PGGSSENCMODE, and ConnectionParams. Through Connection against a fake server, disable and prefer connect, and require, verify-full and gssencmode=require get past validation to their usual connect errors. These tests pass on the previous code too; they guard against rejecting a value that used to be accepted.
  • Rejected values: misspelled, empty, padded and surrogate values are rejected from the DSN, the URI, the environment and both constructors, with the exact message; validate_dsn and parse_dsn agree. The error-order test now includes both modes, and the Connection constructor's NUL and sslcert/sslkey checks are still reported before a mode error.
  • GSS path (Provide structured errors for invalid connection parameters #28 case 3), with the fake GSSAPI library and a server that accepts GSS encryption: gssencmode=require sslmode=bogus gets the ArgumentError from the constructor and the DSN, and Postgres.Error("invalid sslmode: bogus") from API.connect directly. The server never sees a connection. The same configuration with a valid sslmode connects over GSS.
  • Cancel path: API.cancel_request with every sslmode and gssencmode value in mixed case against a fake server: disable/prefer send the CancelRequest, require/verify-full refuse to send it over a server without TLS, and GSS encryption is used for prefer/require. An invalid value is refused before any dial.
  • Changed on purpose: PGSSLMODE="" now fails in parse_dsn (runtests.jl), a surrogate sslmode is no longer accepted by validate_dsn, and Connection(gssencmode = "bogus") throws ArgumentError (gssapi.jl).
  • On the previous code the new tests fail (54 in the connection-string tests, 13 in the GSS tests). Each of these mutations, applied alone, also makes them fail: dropping the parse-time check, either constructor check, or the cancel_request backstop; moving the sslmode check back into _connect_tls! (the direct API.connect test connects over GSS); listing the values again in API.connect, as a tuple or as the old == chain (a source test finds the duplicate list); and moving the constructor's mode check above its NUL checks.
  • Side-by-side against the v2.6.0 parser: 300k generated keyword and URI DSNs (3 seeds), mixing valid modes in random case, misspellings, padding, empty values, NUL, look-alike Unicode, surrogates and malformed bytes, other invalid parameters, and PGSSLMODE/PGGSSENCMODE/PGPORT/PGGSSDELEGATION. Input that both accept parses to the same fields (61,165). Input that both reject gets the same error (157,253). parse_dsn newly rejects 51,837 inputs, each because of a mode that v2.6.0's connect path would have refused. Where an input has several problems and the first error changed (29,745), the new first error is always an invalid mode, reported in its slot before a later parameter, or an invalid sslmode before a malformed gssencmode (312). Nothing newly accepted, validate_dsn never threw, and it always agreed with parse_dsn. 12k ConnectionParams calls matched the old connect path's accept/reject exactly.
  • Full Pkg.test() with Docker integration (POSTGRES_REQUIRE_INTEGRATION=true, PostgreSQL 16, Linux x64), including the trim compile test (no verifier errors): 9919 checks pass on Julia 1.13.2, 9890 on Julia 1.10.12, and 9919 with -t 2.
  • The docs build passes.
  • Fork CI on b50c5bf passed all 14 jobs (Linux/Windows/macOS, Julia min/1/pre, PostgreSQL 14-18, SCRAM/MD5), apart from the Codecov upload, which has no token on the fork.

Drafted with Claude Code (Claude Opus 5.5) and reviewed by me before posting.

🤖 Generated with Claude Code

An invalid sslmode or gssencmode passed parse_dsn and was only rejected
when connecting, and the sslmode check ran in _connect_tls!, after the
GSS branch had already returned: gssencmode=require with an invalid
sslmode connected over GSS encryption.

The accepted values now live in one place, Postgres.SSLMODES and
Postgres.GSSENCMODES. parse_dsn and validate_dsn check them, after
environment defaults are applied, in the slot the malformed-UTF-8 check
already used, with the same message. The ConnectionParams and Connection
keyword constructors check them too and throw the same ArgumentError.
The connect path keeps its checks as a backstop, both before any dial,
and cancel_request gets the same backstop. Values stay case-insensitive
and no value is added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide structured errors for invalid connection parameters

1 participant