From 23483d723fda9160f106cbd0dc89e25fcf8e1f33 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Tue, 1 Apr 2025 16:10:31 +0300 Subject: [PATCH 3/7] libpq: Add min/max_protocol_version connection options All supported version of the PostgreSQL server send the NegotiateProtocolVersion message when an unsupported minor protocol version is requested by a client. But many other applications that implement the PostgreSQL protocol (connection poolers, or other databases) do not, and the same is true for PostgreSQL server versions older than 9.2. Connecting to such other applications thus fails if a client requests a protocol version different than 3.0. This patch adds a max_protocol_version connection option to libpq that specifies the protocol version that libpq should request from the server. Currently all allowed values result in the use of 3.0, but that will be changed in a future commit that bumps the protocol version. Even after that version bump the default will likely stay 3.0 for the time being. Once more of the ecosystem supports the NegotiateProtocolVersion message we might want to change the default to the latest minor version. We also add the similar min_protocol_version connection option, to allow a client to specify that connecting should fail if a lower protocol version is attempted by the server. This can be used to ensure certain protocol features are in used, which can be particularly useful if those features impact security. Author: Jelte Fennema-Nio Reviewed-by: Robert Haas (earlier versions) Discussion: /message-id/CAGECzQTfc_O%2BHXqAo5_-xG4r3EFVsTefUeQzSvhEyyLDba-O9w@mail.gmail.com Discussion: /message-id/CAGECzQRbAGqJnnJJxTdKewTsNOovUt4bsx3NFfofz3m2j-t7tA@mail.gmail.com --- doc/src/sgml/libpq.sgml | 69 +++++++++++++++++- src/interfaces/libpq/fe-connect.c | 104 +++++++++++++++++++++++++++- src/interfaces/libpq/fe-protocol3.c | 11 +++ src/interfaces/libpq/libpq-int.h | 4 ++ 4 files changed, 186 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index b359fbff295..a880e3acf0a 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -2144,6 +2144,54 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + + min_protocol_version + + + Specifies the minimum protocol version to allow for the connection. + The default is to allow any version of the + PostgreSQL protocol supported by libpq, + which currently means 3.0. If the server + does not support at least this protocol version the connection will be + closed. + + + + The current supported values are + 3.0 + and latest. The latest value is + equivalent to the latest protocol version that is supported by the used + libpq version, which currently is 3.0. + + + + + + max_protocol_version + + + Specifies the protocol version to request from the server. + The default is to use version 3.0 of the + PostgreSQL protocol, unless the connection + string specifies a feature that relies on a higher protocol version, + in which case the latest version supported by libpq is used. If the + server does not support the protocol version requested by the client, + the connection is automatically downgraded to a lower minor protocol + version that the server supports. After the connection attempt has + completed you can use to + find out which exact protocol version was negotiated. + + + + The current supported values are + 3.0 + and latest. The latest value is + equivalent to the latest protocol version that is supported by the + libpq version used, which is currently 3.0. + + + + ssl_max_protocol_version @@ -2482,7 +2530,6 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname - @@ -9329,6 +9376,26 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) linkend="libpq-connect-load-balance-hosts"/> connection parameter. + + + + + PGMINPROTOCOLVERSION + + PGMINPROTOCOLVERSION behaves the same as the connection parameter. + + + + + + + PGMAXPROTOCOLVERSION + + PGMAXPROTOCOLVERSION behaves the same as the connection parameter. + + diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index a1e462f2e2f..8f95f47216c 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -325,6 +325,16 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */ offsetof(struct pg_conn, require_auth)}, + {"min_protocol_version", "PGMINPROTOCOLVERSION", + NULL, NULL, + "Min-Protocol-Version", "", 6, /* sizeof("latest") = 6 */ + offsetof(struct pg_conn, min_protocol_version)}, + + {"max_protocol_version", "PGMAXPROTOCOLVERSION", + NULL, NULL, + "Max-Protocol-Version", "", 6, /* sizeof("latest") = 6 */ + offsetof(struct pg_conn, max_protocol_version)}, + {"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL, "SSL-Minimum-Protocol-Version", "", 8, /* sizeof("TLSv1.x") == 8 */ offsetof(struct pg_conn, ssl_min_protocol_version)}, @@ -483,6 +493,7 @@ static void pgpassfileWarning(PGconn *conn); static void default_threadlock(int acquire); static bool sslVerifyProtocolVersion(const char *version); static bool sslVerifyProtocolRange(const char *min, const char *max); +static bool pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, const char *context); /* global variable because fe-auth.c needs to access it */ @@ -2081,6 +2092,42 @@ pqConnectOptions2(PGconn *conn) } } + if (conn->min_protocol_version) + { + if (!pqParseProtocolVersion(conn->min_protocol_version, &conn->min_pversion, conn, "min_protocol_version")) + return false; + } + else + { + conn->min_pversion = PG_PROTOCOL_EARLIEST; + } + + if (conn->max_protocol_version) + { + if (!pqParseProtocolVersion(conn->max_protocol_version, &conn->max_pversion, conn, "max_protocol_version")) + return false; + } + else + { + /* + * To not break connecting to older servers/poolers that do not yet + * support NegotiateProtocolVersion, default to the 3.0 protocol at + * least for a while longer. Except when min_protocol_version is set + * to something larger, then we might as well default to the latest. + */ + if (conn->min_pversion > PG_PROTOCOL(3, 0)) + conn->max_pversion = PG_PROTOCOL_LATEST; + else + conn->max_pversion = PG_PROTOCOL(3, 0); + } + + if (conn->min_pversion > conn->max_pversion) + { + conn->status = CONNECTION_BAD; + libpq_append_conn_error(conn, "min_protocol_version is greater than max_protocol_version"); + return false; + } + /* * Resolve special "auto" client_encoding from the locale */ @@ -3084,7 +3131,7 @@ keep_going: /* We will come back to here until there is * must persist across individual connection attempts, but we must * reset them when we start to consider a new server. */ - conn->pversion = PG_PROTOCOL(3, 0); + conn->pversion = conn->max_pversion; conn->send_appname = true; conn->failed_enc_methods = 0; conn->current_enc_method = 0; @@ -4103,6 +4150,7 @@ keep_going: /* We will come back to here until there is /* OK, we read the message; mark data consumed */ pqParseDone(conn, conn->inCursor); + goto keep_going; } @@ -8158,6 +8206,60 @@ error: return false; } +/* + * Parse and try to interpret "value" as a ProtocolVersion value, and if successful, + * store it in *result. + */ +static bool +pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, + const char *context) +{ + char *end; + int major; + int minor; + ProtocolVersion version; + + if (strcmp(value, "latest") == 0) + { + *result = PG_PROTOCOL_LATEST; + return true; + } + + major = strtol(value, &end, 10); + if (*end != '.') + { + conn->status = CONNECTION_BAD; + libpq_append_conn_error(conn, "invalid %s value: \"%s\"", + context, + value); + return false; + } + + minor = strtol(&end[1], &end, 10); + if (*end != '\0') + { + conn->status = CONNECTION_BAD; + libpq_append_conn_error(conn, "invalid %s value: \"%s\"", + context, + value); + return false; + } + + version = PG_PROTOCOL(major, minor); + if (version > PG_PROTOCOL_LATEST || + version < PG_PROTOCOL_EARLIEST) + { + conn->status = CONNECTION_BAD; + libpq_append_conn_error(conn, "invalid %s value: \"%s\"", + context, + value); + return false; + } + + *result = version; + return true; +} + /* * To keep the API consistent, the locking stubs are always provided, even * if they are not required. diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 43e3519e4bd..5015debca77 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -1444,6 +1444,17 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) goto failure; } + if (their_version < conn->min_pversion) + { + libpq_append_conn_error(conn, "server only supports protocol version %d.%d, but min_protocol_version was set to %d.%d", + PG_PROTOCOL_MAJOR(their_version), + PG_PROTOCOL_MINOR(their_version), + PG_PROTOCOL_MAJOR(conn->min_pversion), + PG_PROTOCOL_MINOR(conn->min_pversion)); + + goto failure; + } + /* the version is acceptable */ conn->pversion = their_version; diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index bf31e660477..68c8cd81d72 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -417,6 +417,8 @@ struct pg_conn char *gsslib; /* What GSS library to use ("gssapi" or * "sspi") */ char *gssdelegation; /* Try to delegate GSS credentials? (0 or 1) */ + char *min_protocol_version; /* minimum used protocol version */ + char *max_protocol_version; /* maximum used protocol version */ char *ssl_min_protocol_version; /* minimum TLS protocol version */ char *ssl_max_protocol_version; /* maximum TLS protocol version */ char *target_session_attrs; /* desired session properties */ @@ -538,6 +540,8 @@ struct pg_conn void *scram_client_key_binary; /* binary SCRAM client key */ size_t scram_server_key_len; void *scram_server_key_binary; /* binary SCRAM server key */ + ProtocolVersion min_pversion; /* protocol version to request */ + ProtocolVersion max_pversion; /* protocol version to request */ /* Miscellaneous stuff */ int be_pid; /* PID of backend --- needed for cancels */ -- 2.39.5