From 993f19fa8d3f8876402716ba4a30aa9373cb6b90 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Fri, 12 Jun 2026 08:50:17 +0000 Subject: [PATCH] libpq: require encrypted connections for OAUTHBEARER Bearer tokens must be protected in transit (RFC 7628, Sec. 5; RFC 6750, Sec. 5.2), but libpq previously sent them to the server over plaintext TCP connections. Refuse the SASL exchange unless the connection uses SSL/TLS or a Unix-domain socket. The previous behavior can be restored for local development and testing with the new unsafe debug option PGOAUTHDEBUG=UNSAFE:plaintext-server. The legacy PGOAUTHDEBUG=UNSAFE format enables it implicitly. Reported-by: Artem Gavrilov --- doc/src/sgml/libpq.sgml | 23 ++++++- src/interfaces/libpq/fe-auth-oauth.c | 15 +++++ src/interfaces/libpq/oauth-debug.h | 10 +++- .../modules/oauth_validator/t/001_server.pl | 60 +++++++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 7d3c3bb66d8..cc21a348aef 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -10275,6 +10275,13 @@ void PQinitSSL(int do_ssl); client application does not have a usable web browser, for example when running a client via SSH. + + Bearer tokens must be protected in transit + (RFC 7628). + libpq refuses to perform OAuth authentication + unless the connection to the server uses + SSL/TLS or a Unix-domain socket. + The builtin flow will, by default, print a URL to visit and a user code to enter there: @@ -10699,6 +10706,19 @@ PGOAUTHDEBUG=UNSAFE legacy format; enables + + plaintext-server (unsafe) + + + Permits sending the OAuth bearer token to the + PostgreSQL server over an unencrypted + connection. Without this option, OAUTHBEARER authentication requires + an SSL/TLS-encrypted connection + or a Unix-domain socket. + + + + call-count (safe) @@ -10724,7 +10744,8 @@ PGOAUTHDEBUG=UNSAFE legacy format; enables Unsafe options (http, trace, - dos-endpoint) require the UNSAFE: prefix. + dos-endpoint, plaintext-server) + require the UNSAFE: prefix. If unsafe options are specified without this prefix, or if an option name is unrecognized, a warning is printed to standard error and that option is ignored. Other valid options in the list continue to work. Safe options diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index 826f7461cb3..b0751716043 100644 --- a/src/interfaces/libpq/fe-auth-oauth.c +++ b/src/interfaces/libpq/fe-auth-oauth.c @@ -1209,6 +1209,21 @@ oauth_exchange(void *opaq, bool final, /* We begin in the initial response phase. */ Assert(inputlen == -1); + /* + * Bearer tokens must be protected in transit (RFC 7628, Sec. 5; + * RFC 6750, Sec. 5.2). Refuse to continue the exchange if the + * connection to the server is neither TLS-encrypted nor local, + * unless the user has explicitly opted into unsafe behavior. + */ + if (!conn->ssl_in_use && + conn->raddr.addr.ss_family != AF_UNIX && + !(oauth_parse_debug_flags() & OAUTHDEBUG_UNSAFE_PLAINTEXT_SERVER)) + { + libpq_append_conn_error(conn, + "OAuth bearer authentication requires an SSL/TLS-encrypted connection (consider sslmode=require)"); + return SASL_FAILED; + } + if (!setup_oauth_parameters(conn)) return SASL_FAILED; diff --git a/src/interfaces/libpq/oauth-debug.h b/src/interfaces/libpq/oauth-debug.h index 4f0c87117e1..07f7703a7f3 100644 --- a/src/interfaces/libpq/oauth-debug.h +++ b/src/interfaces/libpq/oauth-debug.h @@ -35,11 +35,13 @@ */ /* allow HTTP (unencrypted) connections */ -#define OAUTHDEBUG_UNSAFE_HTTP (1<<0) +#define OAUTHDEBUG_UNSAFE_HTTP (1<<0) /* log HTTP traffic (exposes secrets) */ -#define OAUTHDEBUG_UNSAFE_TRACE (1<<1) +#define OAUTHDEBUG_UNSAFE_TRACE (1<<1) /* allow zero-second retry intervals */ -#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT (1<<2) +#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT (1<<2) +/* allow sending the bearer token to the server over plaintext connections */ +#define OAUTHDEBUG_UNSAFE_PLAINTEXT_SERVER (1<<3) /* mind the gap in values; see OAUTHDEBUG_UNSAFE_MASK below */ @@ -112,6 +114,8 @@ oauth_parse_debug_flags(void) flag = OAUTHDEBUG_UNSAFE_TRACE; else if (strcmp(option, "dos-endpoint") == 0) flag = OAUTHDEBUG_UNSAFE_DOS_ENDPOINT; + else if (strcmp(option, "plaintext-server") == 0) + flag = OAUTHDEBUG_UNSAFE_PLAINTEXT_SERVER; else if (strcmp(option, "call-count") == 0) flag = OAUTHDEBUG_CALL_COUNT; else if (strcmp(option, "plugin-errors") == 0) diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl index 1619fbffd45..731df87fa8c 100644 --- a/src/test/modules/oauth_validator/t/001_server.pl +++ b/src/test/modules/oauth_validator/t/001_server.pl @@ -50,6 +50,8 @@ $node->append_conf('postgresql.conf', "oauth_validator_libraries = 'validator'\n"); # Needed to allow connect_fails to inspect postmaster log: $node->append_conf('postgresql.conf', "log_min_messages = debug2"); +# The plaintext-TCP enforcement tests below need a TCP listener. +$node->append_conf('postgresql.conf', "listen_addresses = '127.0.0.1'\n"); $node->start; $node->safe_psql('postgres', 'CREATE USER test;'); @@ -831,6 +833,64 @@ $node->connect_fails( "fail_validator is used for $user", expected_stderr => qr/FATAL:\s+fail_validator: sentinel error/); +# +# Test that bearer tokens are refused over unencrypted TCP connections +# unless the unsafe plaintext-server debug option is set. +# + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf( + 'pg_hba.conf', qq{ +local all test oauth validator=validator issuer="$issuer" scope="openid postgres" +host all test 127.0.0.1/32 oauth validator=validator issuer="$issuer" scope="openid postgres" +}); +$node->reload; + +$log_start = + $node->wait_for_log(qr/reloading configuration files/, $log_start); + +$user = "test"; +my $tcp_connstr = "host=127.0.0.1 user=$user dbname=postgres" + . " oauth_issuer=$issuer oauth_client_id=f02c6361-0635"; + +# The PGOAUTHDEBUG value set above does not include plaintext-server, so +# this must fail before any flow is run. +$node->connect_fails( + $tcp_connstr, + "bearer tokens are not sent over unencrypted TCP", + expected_stderr => + qr@OAuth bearer authentication requires an SSL/TLS-encrypted connection@, + log_unlike => [qr/oauth_validator: token=/]); + +{ + local $ENV{PGOAUTHDEBUG} = "$ENV{PGOAUTHDEBUG},plaintext-server"; + + $node->connect_ok( + $tcp_connstr, + "unencrypted TCP is allowed with plaintext-server", + expected_stderr => + qr@Visit https://example\.com/ and enter the code: postgresuser@, + log_like => [ + qr/connection authenticated: identity="test" method=oauth/, + qr/connection authorized/, + ]); +} + +{ + # The legacy all-options format must keep permitting plaintext. + local $ENV{PGOAUTHDEBUG} = "UNSAFE"; + + $node->connect_ok( + $tcp_connstr, + "unencrypted TCP is allowed with legacy PGOAUTHDEBUG=UNSAFE", + expected_stderr => + qr@Visit https://example\.com/ and enter the code: postgresuser@, + log_like => [ + qr/connection authenticated: identity="test" method=oauth/, + qr/connection authorized/, + ]); +} + # # Test ABI compatibility magic marker # -- 2.43.0