From 49553cfb0bb711a8800a29966b5184dfa1e8607e Mon Sep 17 00:00:00 2001 From: Rahim Kanji Date: Thu, 9 Apr 2026 13:23:56 +0500 Subject: [PATCH] fix: support one-sided TLS version ranges and warn on malformed input parse_tls_version() silently discarded partially-specified ranges like "TLSv1.2-" or "-TLSv1.3", leaving both min and max empty. This meant the user's intent (e.g. "at least TLSv1.2") was lost without any indication. Now one-sided ranges set the specified bound and leave the other side to libpq defaults. A bare "-" logs a proxy_warning. --- docs/pgsql_servers_ssl_params.md | 12 ++++++++ include/Servers_SslParams.h | 13 ++++---- .../unit/pgsql_servers_ssl_params_unit-t.cpp | 30 +++++++++++++++---- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/docs/pgsql_servers_ssl_params.md b/docs/pgsql_servers_ssl_params.md index bb2d629b9..170b41cc5 100644 --- a/docs/pgsql_servers_ssl_params.md +++ b/docs/pgsql_servers_ssl_params.md @@ -49,6 +49,14 @@ Controls which TLS protocol versions are allowed for backend connections. This m Allows connections using any TLS version from `min_version` to `max_version` inclusive. +**Min-only:** `-` + +Sets a minimum TLS version with no upper bound (libpq default max applies). + +**Max-only:** `-` + +Sets a maximum TLS version. The minimum defaults to libpq's built-in default (`TLSv1.2`). + **Single version (pin):** `` Pins to exactly that TLS version. Both min and max are set to the same value. @@ -57,6 +65,8 @@ Pins to exactly that TLS version. Both min and max are set to the same value. Uses libpq defaults (no restriction). +A bare `-` is treated as malformed and ignored (a warning is logged). + ### Valid Version Tokens `TLSv1`, `TLSv1.1`, `TLSv1.2`, `TLSv1.3` @@ -70,6 +80,8 @@ Uses libpq defaults (no restriction). | `TLSv1.2-TLSv1.3` | Allow TLS 1.2 and TLS 1.3 | | `TLSv1.3` | Pin to TLS 1.3 only | | `TLSv1.2-TLSv1.2` | Pin to TLS 1.2 only (equivalent to `TLSv1.2`) | +| `TLSv1.2-` | Require at least TLS 1.2 (max defaults to highest OpenSSL supports) | +| `-TLSv1.3` | Allow up to TLS 1.3 (min defaults to libpq's built-in `TLSv1.2`) | | `''` (empty) | Use libpq defaults | ## Lookup Hierarchy diff --git a/include/Servers_SslParams.h b/include/Servers_SslParams.h index 1adc5530c..006d33090 100644 --- a/include/Servers_SslParams.h +++ b/include/Servers_SslParams.h @@ -103,8 +103,8 @@ class PgSQLServers_SslParams : public Servers_SslParams { private: // Parse tls_version into ssl_min_protocol_version / ssl_max_protocol_version. - // Format: "MIN-MAX" for a range, or a single token to pin both ends. - // Empty or malformed values leave both fields empty. + // Format: "MIN-MAX" for a range, "MIN-" for min-only, "-MAX" for max-only, + // or a single token to pin both ends. A bare "-" is malformed and logged. void parse_tls_version() { if (tls_version.empty()) return; size_t dash_pos = tls_version.find('-'); @@ -115,10 +115,13 @@ class PgSQLServers_SslParams : public Servers_SslParams { } string min_ver = tls_version.substr(0, dash_pos); string max_ver = tls_version.substr(dash_pos + 1); - if (!min_ver.empty() && !max_ver.empty()) { - ssl_min_protocol_version = min_ver; - ssl_max_protocol_version = max_ver; + if (min_ver.empty() && max_ver.empty()) { + proxy_warning("Malformed ssl_protocol_version_range '%s' for %s:%d — ignoring\n", + tls_version.c_str(), hostname.c_str(), port); + return; } + ssl_min_protocol_version = min_ver; + ssl_max_protocol_version = max_ver; } }; diff --git a/test/tap/tests/unit/pgsql_servers_ssl_params_unit-t.cpp b/test/tap/tests/unit/pgsql_servers_ssl_params_unit-t.cpp index e1a453363..243016afc 100644 --- a/test/tap/tests/unit/pgsql_servers_ssl_params_unit-t.cpp +++ b/test/tap/tests/unit/pgsql_servers_ssl_params_unit-t.cpp @@ -160,15 +160,35 @@ static void test_pgsql_derived_class() { ok(empty.ssl_min_protocol_version == "", "PgSQL derived: empty tls_version -> empty min"); ok(empty.ssl_max_protocol_version == "", "PgSQL derived: empty tls_version -> empty max"); - // Malformed (one side missing) leaves both empty - PgSQLServers_SslParams bad( + // One-sided range: min-only ("TLSv1.2-") sets min, leaves max empty + PgSQLServers_SslParams min_only( string("bh"), 5432, string(""), string(""), string(""), string(""), string(""), string(""), string("TLSv1.2-"), string("") ); - ok(bad.ssl_min_protocol_version == "", "PgSQL derived: malformed -> empty min"); - ok(bad.ssl_max_protocol_version == "", "PgSQL derived: malformed -> empty max"); + ok(min_only.ssl_min_protocol_version == "TLSv1.2", "PgSQL derived: min-only range -> min set"); + ok(min_only.ssl_max_protocol_version == "", "PgSQL derived: min-only range -> max empty"); + + // One-sided range: max-only ("-TLSv1.3") sets max, leaves min empty + PgSQLServers_SslParams max_only( + string("bh2"), 5432, string(""), + string(""), string(""), string(""), + string(""), string(""), + string("-TLSv1.3"), string("") + ); + ok(max_only.ssl_min_protocol_version == "", "PgSQL derived: max-only range -> min empty"); + ok(max_only.ssl_max_protocol_version == "TLSv1.3", "PgSQL derived: max-only range -> max set"); + + // Bare dash is malformed: both stay empty (logged as warning) + PgSQLServers_SslParams bare_dash( + string("bh3"), 5432, string(""), + string(""), string(""), string(""), + string(""), string(""), + string("-"), string("") + ); + ok(bare_dash.ssl_min_protocol_version == "", "PgSQL derived: bare dash -> empty min"); + ok(bare_dash.ssl_max_protocol_version == "", "PgSQL derived: bare dash -> empty max"); } static void test_pgsql_storable_in_map() { @@ -314,7 +334,7 @@ static void test_lookup_different_host() { // ============================================================================ int main() { - plan(67); + plan(71); test_init_minimal();