public inbox for [email protected]
help / color / mirror / Atom feed[oauth] Split and extend PGOAUTHDEBUG
13+ messages / 3 participants
[nested] [flat]
* [oauth] Split and extend PGOAUTHDEBUG
@ 2026-02-18 15:07 Zsolt Parragi <[email protected]>
0 siblings, 1 reply; 13+ messages in thread
From: Zsolt Parragi @ 2026-02-18 15:07 UTC (permalink / raw)
To: PostgreSQL Hackers <[email protected]>
Hello!
I'm proposing 2 patches:
1 is the same patch I already sent as part of the PGOAUTHCAFILE
discussion[1], rebased on the current master: it splits
PGOAUTHDEBUG=UNSAFE into separate unsafe/safe settings which users can
toggle one by one.
2 is a new unsafe setting issuer-mismatch, which allows a connection
to continue if the client and server issuers don't match. While this
isn't useful for end users, it makes testing validators easier, as
validators authors should be able to verify that mismatched
configurations are rejected properly by the validator.
I based 2 on 1 because unconditionally adding this new unsafe option
would conflict with some tests. This way that test can use a limited
subset of PGOAUTHDEBUG and still work as intended.
Even in this form it is a best effort, as this is a debugging/testing flag:
a. If a custom client uses a custom PG_AUTHDATA_HOOK and provides a
custom token, libpq accepts any issues URL
b. If the issuer is a well known URI, used directly by libpq, it
accept the URL as is
c. if the url is not a well known URI, but doesn't match the server
URI - it doesn't work that nicely, it accepts the difference but
continues but retrieves the well known URI from the server, so ignores
the client setting
Technically this was already possible by a variation of (a) without
this patch, by implementing a custom client with a PG_AUTHDATA_HOOK,
providing a token from a different issuer to it, and lying about the
issuer to libpq (providing what the server expects). But that's not an
easy way to do it and requires all validators to implement custom
clients for testing.
Additionally this feature also could be useful for demoing that
validators are secure to users ("see, the validator rejects the
request even if we trick the client into continuing with
authentication")
[1] : https://www.postgresql.org/message-id/CAN4CZFNvZ9%2BpQ%3DOA4m%3DHcDgip84GHnekh4gUhYWfK3Q4%2BrBMxA%40...
Attachments:
[application/octet-stream] 0002-Add-new-PGOAUTHDEBUG-option-issuer-mismatch.patch (7.3K, 2-0002-Add-new-PGOAUTHDEBUG-option-issuer-mismatch.patch)
download | inline diff:
From 8f4331cfbb099e4b641a5b31f55a05be969915df Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <[email protected]>
Date: Wed, 18 Feb 2026 14:51:46 +0100
Subject: [PATCH 2/2] Add new PGOAUTHDEBUG option: issuer-mismatch
This new unsafe option allows to connection to proceed if the issuer
configured on the server and client mismatch, allowing to write
mismatched-issuer tests for validators.
Validators should test scenarios like this, as the wire allows this
situation, but previously libpq/psql prevented it, making writing tests
for this more difficult.
---
doc/src/sgml/libpq.sgml | 16 +++++++++++++++-
src/interfaces/libpq-oauth/oauth-curl.c | 13 +++++++++----
src/interfaces/libpq/fe-auth-oauth-debug.c | 7 +++++++
src/interfaces/libpq/fe-auth-oauth.c | 18 +++++++++++-------
src/interfaces/libpq/fe-auth-oauth.h | 1 +
.../modules/oauth_validator/t/001_server.pl | 15 +++++++++------
6 files changed, 52 insertions(+), 18 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 5d70cc2b261..6f29a4a7756 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10559,6 +10559,20 @@ PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</linea
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>issuer-mismatch</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Tolerates a mismatch between the client's configured
+ <literal>oauth_issuer</literal> and the issuer found in the server's
+ discovery document. This disables the mix-up attack protection from
+ RFC 9207 and should only be used in development or testing environments
+ where the server's issuer identifier does not match the client
+ configuration.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>fast-retry</literal> (safe)</term>
<listitem>
@@ -10595,7 +10609,7 @@ PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</linea
<para>
Unsafe options (<literal>http</literal>, <literal>trace</literal>,
- <literal>custom-ca</literal>) require the <literal>UNSAFE:</literal> prefix.
+ <literal>custom-ca</literal>, <literal>issuer-mismatch</literal>) require the <literal>UNSAFE:</literal> prefix.
If unsafe options are specified without this prefix, a warning is printed
to standard error and that option is ignored. Other valid options in the
list continue to work. Safe options (<literal>fast-retry</literal>,
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index ac8b4631d53..d9512ef17dd 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -2223,10 +2223,15 @@ check_issuer(struct async_ctx *actx, PGconn *conn)
*/
if (strcmp(oauth_issuer_id, provider->issuer) != 0)
{
- actx_error(actx,
- "the issuer identifier (%s) does not match oauth_issuer (%s)",
- provider->issuer, oauth_issuer_id);
- return false;
+ if (!actx->debug_flags.issuer_mismatch)
+ {
+ actx_error(actx,
+ "the issuer identifier (%s) does not match oauth_issuer (%s)",
+ provider->issuer, oauth_issuer_id);
+ return false;
+ }
+
+ return true;
}
return true;
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
index f65f069fed8..558b34da561 100644
--- a/src/interfaces/libpq/fe-auth-oauth-debug.c
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -53,6 +53,12 @@ parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe
*is_unsafe = true;
return true;
}
+ else if (strcmp(option, "issuer-mismatch") == 0)
+ {
+ flags->issuer_mismatch = true;
+ *is_unsafe = true;
+ return true;
+ }
/* Safe options */
else if (strcmp(option, "fast-retry") == 0)
{
@@ -103,6 +109,7 @@ oauth_get_debug_flags(void)
flags.http = true;
flags.trace = true;
flags.custom_ca = true;
+ flags.issuer_mismatch = true;
flags.fast_retry = true;
flags.poll_counts = true;
flags.print_plugin_errors = true;
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 5dff354c19b..b6d472a0330 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -606,13 +606,16 @@ handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen)
if (strcmp(conn->oauth_issuer_id, discovery_issuer) != 0)
{
- libpq_append_conn_error(conn,
- "server's discovery document at %s (issuer \"%s\") is incompatible with oauth_issuer (%s)",
- ctx.discovery_uri, discovery_issuer,
- conn->oauth_issuer_id);
+ if (!oauth_get_debug_flags().issuer_mismatch)
+ {
+ libpq_append_conn_error(conn,
+ "server's discovery document at %s (issuer \"%s\") is incompatible with oauth_issuer (%s)",
+ ctx.discovery_uri, discovery_issuer,
+ conn->oauth_issuer_id);
- free(discovery_issuer);
- goto cleanup;
+ free(discovery_issuer);
+ goto cleanup;
+ }
}
free(discovery_issuer);
@@ -625,7 +628,8 @@ handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen)
else
{
/* This must match the URI we'd previously determined. */
- if (strcmp(conn->oauth_discovery_uri, ctx.discovery_uri) != 0)
+ if (strcmp(conn->oauth_discovery_uri, ctx.discovery_uri) != 0
+ && !oauth_get_debug_flags().issuer_mismatch)
{
libpq_append_conn_error(conn,
"server's discovery document has moved to %s (previous location was %s)",
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 272638ea359..918681f16a5 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -52,6 +52,7 @@ typedef struct oauth_debug_flags
bool http; /* allow HTTP (unencrypted) connections */
bool trace; /* log HTTP traffic (exposes secrets) */
bool custom_ca; /* allow custom CA certificate file */
+ bool issuer_mismatch; /* tolerate issuer mismatch */
/* SAFE features - allowed without UNSAFE: prefix */
bool fast_retry; /* allow zero-second retry intervals */
diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index 6b649c0b06f..b587b51bb19 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -136,12 +136,15 @@ $node->connect_ok(
]);
# The issuer linked by the server must match the client's oauth_issuer setting.
-$node->connect_fails(
- "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0636",
- "oauth_issuer must match discovery",
- expected_stderr =>
- qr@server's discovery document at \Q$issuer/.well-known/oauth-authorization-server/alternate\E \(issuer "\Q$issuer/alternate\E"\) is incompatible with oauth_issuer \(\Q$issuer\E\)@
-);
+{
+ local $ENV{PGOAUTHDEBUG} = "UNSAFE:http";
+ $node->connect_fails(
+ "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0636",
+ "oauth_issuer must match discovery",
+ expected_stderr =>
+ qr@server's discovery document at \Q$issuer/.well-known/oauth-authorization-server/alternate\E \(issuer "\Q$issuer/alternate\E"\) is incompatible with oauth_issuer \(\Q$issuer\E\)@
+ );
+}
# Test require_auth settings against OAUTHBEARER.
my @cases = (
--
2.43.0
[application/octet-stream] 0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch (20.6K, 3-0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch)
download | inline diff:
From 9eec791d666bebf3735ccb286e6f044f391f85fd Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <[email protected]>
Date: Thu, 11 Dec 2025 23:56:08 +0000
Subject: [PATCH 1/2] Split PGOAUTHDEBUG=UNSAFE into multiple options
---
doc/src/sgml/libpq.sgml | 144 ++++++++++++++----
src/interfaces/libpq-oauth/Makefile | 12 +-
src/interfaces/libpq-oauth/meson.build | 6 +-
src/interfaces/libpq-oauth/oauth-curl.c | 18 +--
src/interfaces/libpq-oauth/oauth-utils.c | 11 --
src/interfaces/libpq-oauth/oauth-utils.h | 2 +-
src/interfaces/libpq-oauth/test-oauth-curl.c | 8 +-
src/interfaces/libpq/Makefile | 3 +-
src/interfaces/libpq/fe-auth-oauth-debug.c | 147 +++++++++++++++++++
src/interfaces/libpq/fe-auth-oauth.c | 16 +-
src/interfaces/libpq/fe-auth-oauth.h | 19 ++-
src/interfaces/libpq/meson.build | 1 +
12 files changed, 317 insertions(+), 70 deletions(-)
create mode 100644 src/interfaces/libpq/fe-auth-oauth-debug.c
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 21e1ba34a4e..5d70cc2b261 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10508,41 +10508,123 @@ typedef struct PGoauthBearerRequest
<title>Debugging and Developer Settings</title>
<para>
- A "dangerous debugging mode" may be enabled by setting the environment
- variable <envar>PGOAUTHDEBUG=UNSAFE</envar>. This functionality is provided
- for ease of local development and testing only. It does several things that
- you will not want a production system to do:
+ Debug features may be enabled by setting the <envar>PGOAUTHDEBUG</envar>
+ environment variable. This functionality is provided for ease of local
+ development and testing. The variable accepts a comma-separated list of
+ debug options:
+
+ <programlisting>
+PGOAUTHDEBUG=option1,option2,... <lineannotation>for safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:option1,option2,... <lineannotation>when using unsafe options</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</lineannotation>
+ </programlisting>
+ </para>
- <itemizedlist spacing="compact">
- <listitem>
- <para>
- permits the use of unencrypted HTTP during the OAuth provider exchange
- </para>
- </listitem>
- <listitem>
- <para>
- allows the system's trusted CA list to be completely replaced using the
- <envar>PGOAUTHCAFILE</envar> environment variable
- </para>
- </listitem>
- <listitem>
- <para>
- prints HTTP traffic (containing several critical secrets) to standard
- error during the OAuth flow
- </para>
- </listitem>
- <listitem>
- <para>
- permits the use of zero-second retry intervals, which can cause the
- client to busy-loop and pointlessly consume CPU
- </para>
- </listitem>
- </itemizedlist>
+ <para>
+ Available debug options:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>http</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Permits the use of unencrypted HTTP during the OAuth provider exchange.
+ This allows OAuth credentials to be transmitted over unencrypted
+ connections, which is extremely dangerous and should only be used for
+ local testing.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>trace</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Prints HTTP traffic to standard error during the OAuth flow. This output
+ contains critical secrets including bearer tokens, client secrets, access
+ tokens, and authorization codes. Never share this output with third
+ parties.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>custom-ca</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Allows the system's trusted CA list to be completely replaced using the
+ <envar>PGOAUTHCAFILE</envar> environment variable. This can facilitate
+ man-in-the-middle attacks when testing with self-signed certificates.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>fast-retry</literal> (safe)</term>
+ <listitem>
+ <para>
+ Permits the use of zero-second retry intervals instead of the normal
+ minimum of one second. This can speed up tests but may cause the client
+ to busy-loop and consume CPU unnecessarily.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>poll-counts</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints the total number of poll() calls to standard error when the
+ OAuth flow completes. This helps developers debug the async multiplexer
+ behavior.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>print-plugin-errors</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints plugin loading errors to standard error. This helps developers
+ and package maintainers debug issues when the OAuth plugin fails to load.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+
+ <para>
+ Unsafe options (<literal>http</literal>, <literal>trace</literal>,
+ <literal>custom-ca</literal>) require the <literal>UNSAFE:</literal> prefix.
+ If unsafe options are specified without this prefix, a warning is printed
+ to standard error and that option is ignored. Other valid options in the
+ list continue to work. Safe options (<literal>fast-retry</literal>,
+ <literal>poll-counts</literal>, <literal>print-plugin-errors</literal>) can
+ be used without the prefix.
+ </para>
+
+ <para>
+ Unrecognized option names will also trigger a warning and be ignored, while
+ valid options continue to work. This helps catch typos in the environment
+ variable configuration without breaking the debugging of valid options.
</para>
+
+ <para>
+ Examples:
+ <programlisting>
+PGOAUTHDEBUG=fast-retry,poll-counts <lineannotation>safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,trace <lineannotation>enable HTTP and traffic logging</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,custom-ca,poll-counts <lineannotation>mix of unsafe and safe</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy; enables all options</lineannotation>
+ </programlisting>
+ </para>
+
<warning>
<para>
- Do not share the output of the OAuth flow traffic with third parties. It
- contains secrets that can be used to attack your clients and servers.
+ Never use unsafe debug options in production environments. The
+ <literal>trace</literal> option in particular exposes secrets that can be
+ used to attack your clients and servers. Do not share the output with third
+ parties.
</para>
</warning>
</sect2>
diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile
index a5f2d65fcad..60ce0ad8c58 100644
--- a/src/interfaces/libpq-oauth/Makefile
+++ b/src/interfaces/libpq-oauth/Makefile
@@ -30,15 +30,25 @@ override CFLAGS += $(PTHREAD_CFLAGS)
OBJS = \
$(WIN32RES)
-OBJS_STATIC = oauth-curl.o
+OBJS_STATIC = \
+ oauth-curl.o \
+ fe-auth-oauth-debug.o
# The shared library needs additional glue symbols.
OBJS_SHLIB = \
oauth-curl_shlib.o \
oauth-utils.o \
+ fe-auth-oauth-debug_shlib.o
oauth-utils.o: override CPPFLAGS += -DUSE_DYNAMIC_OAUTH
oauth-curl_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH
+fe-auth-oauth-debug_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH
+
+fe-auth-oauth-debug.o: $(libpq_srcdir)/fe-auth-oauth-debug.c
+ $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
+
+fe-auth-oauth-debug_shlib.o: $(libpq_srcdir)/fe-auth-oauth-debug.c
+ $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@
# Add shlib-/stlib-specific objects.
$(shlib): override OBJS += $(OBJS_SHLIB)
diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build
index d8a0c04095a..86a10ccca27 100644
--- a/src/interfaces/libpq-oauth/meson.build
+++ b/src/interfaces/libpq-oauth/meson.build
@@ -6,6 +6,7 @@ endif
libpq_oauth_sources = files(
'oauth-curl.c',
+ '../libpq/fe-auth-oauth-debug.c',
)
# The shared library needs additional glue symbols.
@@ -50,7 +51,10 @@ libpq_oauth_so = shared_module(libpq_oauth_name,
libpq_oauth_test_deps = []
-oauth_test_sources = files('test-oauth-curl.c') + libpq_oauth_so_sources
+oauth_test_sources = files(
+ 'test-oauth-curl.c',
+ '../libpq/fe-auth-oauth-debug.c',
+) + libpq_oauth_so_sources
if host_system == 'windows'
oauth_test_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 691e7ec1d9f..ac8b4631d53 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -278,7 +278,7 @@ struct async_ctx
int running; /* is asynchronous work in progress? */
bool user_prompted; /* have we already sent the authz prompt? */
bool used_basic_auth; /* did we send a client secret? */
- bool debugging; /* can we give unsafe developer assistance? */
+ oauth_debug_flags debug_flags; /* can we give developer assistance */
int dbg_num_calls; /* (debug mode) how many times were we called? */
};
@@ -985,7 +985,7 @@ parse_interval(struct async_ctx *actx, const char *interval_str)
parsed = ceil(parsed);
if (parsed < 1)
- return actx->debugging ? 0 : 1;
+ return actx->debug_flags.fast_retry ? 0 : 1;
else if (parsed >= INT_MAX)
return INT_MAX;
@@ -1759,7 +1759,7 @@ setup_curl_handles(struct async_ctx *actx)
*/
CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
- if (actx->debugging)
+ if (actx->debug_flags.trace)
{
/*
* Set a callback for retrieving error information from libcurl, the
@@ -1791,7 +1791,7 @@ setup_curl_handles(struct async_ctx *actx)
const long unsafe = CURLPROTO_HTTPS | CURLPROTO_HTTP;
#endif
- if (actx->debugging)
+ if (actx->debug_flags.http)
protos = unsafe;
CHECK_SETOPT(actx, popt, protos, return false);
@@ -1805,7 +1805,7 @@ setup_curl_handles(struct async_ctx *actx)
* the flow to work at all, so any changes to the roots are likely to be
* done system-wide.
*/
- if (actx->debugging)
+ if (actx->debug_flags.custom_ca)
{
const char *env;
@@ -2271,7 +2271,7 @@ check_for_device_flow(struct async_ctx *actx)
* decent time to bail out if we're not using HTTPS for the endpoints
* we'll use for the flow.
*/
- if (!actx->debugging)
+ if (!actx->debug_flags.http)
{
if (pg_strncasecmp(provider->device_authorization_endpoint,
HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
@@ -2793,8 +2793,8 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
actx->mux = PGINVALID_SOCKET;
actx->timerfd = -1;
- /* Should we enable unsafe features? */
- actx->debugging = oauth_unsafe_debugging_enabled();
+ /* Parse debug flags from environment */
+ actx->debug_flags = oauth_get_debug_flags();
state->async_ctx = actx;
@@ -3074,7 +3074,7 @@ pg_fe_run_oauth_flow(PGconn *conn)
actx = state->async_ctx;
Assert(actx || result == PGRES_POLLING_FAILED);
- if (actx && actx->debugging)
+ if (actx && actx->debug_flags.poll_counts)
{
actx->dbg_num_calls++;
if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c
index 4ebe7d0948c..fab8990b746 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.c
+++ b/src/interfaces/libpq-oauth/oauth-utils.c
@@ -142,17 +142,6 @@ libpq_gettext(const char *msgid)
#endif /* ENABLE_NLS */
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
-
/*
* Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by
* pq_block/reset_sigpipe().
diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h
index 9f4d5b692d2..4d986fcb358 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.h
+++ b/src/interfaces/libpq-oauth/oauth-utils.h
@@ -76,7 +76,7 @@ typedef enum
} PGTernaryBool;
extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3);
-extern bool oauth_unsafe_debugging_enabled(void);
+extern oauth_debug_flags oauth_get_debug_flags(void);
extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe);
diff --git a/src/interfaces/libpq-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c
index 4328a332738..d9971797b5c 100644
--- a/src/interfaces/libpq-oauth/test-oauth-curl.c
+++ b/src/interfaces/libpq-oauth/test-oauth-curl.c
@@ -89,7 +89,13 @@ init_test_actx(void)
actx->mux = PGINVALID_SOCKET;
actx->timerfd = -1;
- actx->debugging = true;
+ actx->debug_flags.http = true;
+ actx->debug_flags.trace = true;
+ actx->debug_flags.custom_ca = true;
+ actx->debug_flags.issuer_mismatch = true;
+ actx->debug_flags.fast_retry = true;
+ actx->debug_flags.poll_counts = true;
+ actx->debug_flags.print_plugin_errors = true;
initPQExpBuffer(&actx->errbuf);
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index bf4baa92917..1165859859c 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -44,7 +44,8 @@ OBJS = \
legacy-pqsignal.o \
libpq-events.o \
pqexpbuffer.o \
- fe-auth.o
+ fe-auth.o \
+ fe-auth-oauth-debug.o
# File shared across all SSL implementations supported.
ifneq ($(with_ssl),no)
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
new file mode 100644
index 00000000000..f65f069fed8
--- /dev/null
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -0,0 +1,147 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-auth-oauth-debug.c
+ * Parsing logic for PGOAUTHDEBUG environment variable
+ *
+ * This file contains pure string parsing logic with no dependencies on
+ * libpq or libpq-oauth implementation details. It's compiled into both
+ * libraries to avoid code duplication.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/fe-auth-oauth-debug.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fe-auth-oauth.h"
+
+/*
+ * Parse a single debug option from PGOAUTHDEBUG.
+ * Returns true if the option is recognized, false otherwise.
+ * Sets *is_unsafe to indicate if this option requires the UNSAFE: prefix.
+ */
+static bool
+parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe)
+{
+ *is_unsafe = false;
+
+ /* Unsafe options */
+ if (strcmp(option, "http") == 0)
+ {
+ flags->http = true;
+ *is_unsafe = true;
+ return true;
+ }
+ else if (strcmp(option, "trace") == 0)
+ {
+ flags->trace = true;
+ *is_unsafe = true;
+ return true;
+ }
+ else if (strcmp(option, "custom-ca") == 0)
+ {
+ flags->custom_ca = true;
+ *is_unsafe = true;
+ return true;
+ }
+ /* Safe options */
+ else if (strcmp(option, "fast-retry") == 0)
+ {
+ flags->fast_retry = true;
+ return true;
+ }
+ else if (strcmp(option, "poll-counts") == 0)
+ {
+ flags->poll_counts = true;
+ return true;
+ }
+ else if (strcmp(option, "print-plugin-errors") == 0)
+ {
+ flags->print_plugin_errors = true;
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * Parses the PGOAUTHDEBUG environment variable and returns debug flags.
+ *
+ * Supported formats:
+ * PGOAUTHDEBUG=UNSAFE - legacy format, enables all features
+ * PGOAUTHDEBUG=option1,option2 - enable safe features only
+ * PGOAUTHDEBUG=UNSAFE:opt1,opt2 - enable unsafe and/or safe features
+ *
+ * Prints a warning and skips the invalid option if:
+ * - An unrecognized option is specified
+ * - An unsafe option is specified without the UNSAFE: prefix
+ */
+oauth_debug_flags
+oauth_get_debug_flags(void)
+{
+ oauth_debug_flags flags = {0};
+ const char *env = getenv("PGOAUTHDEBUG");
+ char *options_str;
+ char *option;
+ char *saveptr = NULL;
+ bool unsafe_prefix = false;
+
+ if (!env || env[0] == '\0')
+ return flags;
+
+ if (strcmp(env, "UNSAFE") == 0)
+ {
+ flags.http = true;
+ flags.trace = true;
+ flags.custom_ca = true;
+ flags.fast_retry = true;
+ flags.poll_counts = true;
+ flags.print_plugin_errors = true;
+ return flags;
+ }
+
+ if (strncmp(env, "UNSAFE:", 7) == 0)
+ {
+ unsafe_prefix = true;
+ env += 7;
+ }
+
+ options_str = strdup(env);
+ if (!options_str)
+ return flags;
+
+ option = strtok_r(options_str, ",", &saveptr);
+ while (option != NULL)
+ {
+ bool is_unsafe;
+
+ if (!parse_debug_option(option, &flags, &is_unsafe))
+ {
+ fprintf(stderr,
+ "WARNING: PGOAUTHDEBUG: unrecognized debug option \"%s\" (ignored)\n",
+ option);
+ }
+ else if (is_unsafe && !unsafe_prefix)
+ {
+ fprintf(stderr,
+ "WARNING: PGOAUTHDEBUG: unsafe option \"%s\" requires UNSAFE: prefix (ignored)\n"
+ "Use: PGOAUTHDEBUG=UNSAFE:%s\n",
+ option, option);
+ }
+
+ option = strtok_r(NULL, ",", &saveptr);
+ }
+
+ free(options_str);
+
+ return flags;
+}
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 67879d64b39..5dff354c19b 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -383,7 +383,7 @@ issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
authority_start = wkuri + strlen(HTTPS_SCHEME);
if (!authority_start
- && oauth_unsafe_debugging_enabled()
+ && oauth_get_debug_flags().http
&& pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
{
/* Allow http:// for testing only. */
@@ -877,7 +877,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state)
*
* Note that POSIX dlerror() isn't guaranteed to be threadsafe.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_get_debug_flags().print_plugin_errors)
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
return false;
@@ -891,7 +891,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state)
* This is more of an error condition than the one above, but due to
* the dlerror() threadsafety issue, lock it behind PGOAUTHDEBUG too.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_get_debug_flags().print_plugin_errors)
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->builtin_flow);
@@ -1392,13 +1392,3 @@ pqClearOAuthToken(PGconn *conn)
conn->oauth_token = NULL;
}
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 5c8a24b76fa..272638ea359 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -42,8 +42,25 @@ typedef struct
void *builtin_flow;
} fe_oauth_state;
+/*
+ * Debug flags for PGOAUTHDEBUG environment variable.
+ * Each flag controls a specific debug feature.
+ */
+typedef struct oauth_debug_flags
+{
+ /* UNSAFE features - require UNSAFE: prefix */
+ bool http; /* allow HTTP (unencrypted) connections */
+ bool trace; /* log HTTP traffic (exposes secrets) */
+ bool custom_ca; /* allow custom CA certificate file */
+
+ /* SAFE features - allowed without UNSAFE: prefix */
+ bool fast_retry; /* allow zero-second retry intervals */
+ bool poll_counts; /* print poll() statistics */
+ bool print_plugin_errors; /* print plugin loading errors */
+} oauth_debug_flags;
+
extern void pqClearOAuthToken(PGconn *conn);
-extern bool oauth_unsafe_debugging_enabled(void);
+extern oauth_debug_flags oauth_get_debug_flags(void);
extern bool use_builtin_flow(PGconn *conn, fe_oauth_state *state);
/* Mechanisms in fe-auth-oauth.c */
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index c5ecd9c3a87..7f2999aebb3 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -2,6 +2,7 @@
libpq_sources = files(
'fe-auth-oauth.c',
+ 'fe-auth-oauth-debug.c',
'fe-auth-scram.c',
'fe-auth.c',
'fe-cancel.c',
--
2.43.0
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-03-30 21:41 Jacob Champion <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 1 reply; 13+ messages in thread
From: Jacob Champion @ 2026-03-30 21:41 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Wed, Feb 18, 2026 at 7:08 AM Zsolt Parragi <[email protected]> wrote:
> 1 is the same patch I already sent as part of the PGOAUTHCAFILE
> discussion[1], rebased on the current master: it splits
> PGOAUTHDEBUG=UNSAFE into separate unsafe/safe settings which users can
> toggle one by one.
>
> 2 is a new unsafe setting issuer-mismatch, which allows a connection
> to continue if the client and server issuers don't match. While this
> isn't useful for end users, it makes testing validators easier, as
> validators authors should be able to verify that mismatched
> configurations are rejected properly by the validator.
v2, attached, rebases this over 993368113. The big change is the
removal of `custom-ca`; there were a couple of other tweaks to get
both commits compiling independently.
--Jacob
Attachments:
[application/octet-stream] since-v1.nocfbot.diff (14.2K, 2-since-v1.nocfbot.diff)
download | inline diff:
1: 5853d79e0d4 ! 1: e51f717e07c Split PGOAUTHDEBUG=UNSAFE into multiple options
@@ Commit message
Split PGOAUTHDEBUG=UNSAFE into multiple options
## doc/src/sgml/libpq.sgml ##
-@@ doc/src/sgml/libpq.sgml: typedef struct PGoauthBearerRequest
- <title>Debugging and Developer Settings</title>
+@@ doc/src/sgml/libpq.sgml: typedef struct
+ </para>
<para>
- A "dangerous debugging mode" may be enabled by setting the environment
@@ doc/src/sgml/libpq.sgml: typedef struct PGoauthBearerRequest
- </listitem>
- <listitem>
- <para>
-- allows the system's trusted CA list to be completely replaced using the
-- <envar>PGOAUTHCAFILE</envar> environment variable
-- </para>
-- </listitem>
-- <listitem>
-- <para>
- prints HTTP traffic (containing several critical secrets) to standard
- error during the OAuth flow
- </para>
@@ doc/src/sgml/libpq.sgml: typedef struct PGoauthBearerRequest
+ </varlistentry>
+
+ <varlistentry>
-+ <term><literal>custom-ca</literal> (unsafe)</term>
-+ <listitem>
-+ <para>
-+ Allows the system's trusted CA list to be completely replaced using the
-+ <envar>PGOAUTHCAFILE</envar> environment variable. This can facilitate
-+ man-in-the-middle attacks when testing with self-signed certificates.
-+ </para>
-+ </listitem>
-+ </varlistentry>
-+
-+ <varlistentry>
+ <term><literal>fast-retry</literal> (safe)</term>
+ <listitem>
+ <para>
@@ doc/src/sgml/libpq.sgml: typedef struct PGoauthBearerRequest
+ </para>
+
+ <para>
-+ Unsafe options (<literal>http</literal>, <literal>trace</literal>,
-+ <literal>custom-ca</literal>) require the <literal>UNSAFE:</literal> prefix.
++ Unsafe options (<literal>http</literal>, <literal>trace</literal>)
++ require the <literal>UNSAFE:</literal> prefix.
+ If unsafe options are specified without this prefix, a warning is printed
+ to standard error and that option is ignored. Other valid options in the
+ list continue to work. Safe options (<literal>fast-retry</literal>,
+ <literal>poll-counts</literal>, <literal>print-plugin-errors</literal>) can
+ be used without the prefix.
-+ </para>
+ </para>
+
+ <para>
+ Unrecognized option names will also trigger a warning and be ignored, while
+ valid options continue to work. This helps catch typos in the environment
+ variable configuration without breaking the debugging of valid options.
- </para>
++ </para>
+
+ <para>
+ Examples:
+ <programlisting>
+PGOAUTHDEBUG=fast-retry,poll-counts <lineannotation>safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,trace <lineannotation>enable HTTP and traffic logging</lineannotation>
-+PGOAUTHDEBUG=UNSAFE:http,custom-ca,poll-counts <lineannotation>mix of unsafe and safe</lineannotation>
++PGOAUTHDEBUG=UNSAFE:http,poll-counts <lineannotation>mix of unsafe and safe</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy; enables all options</lineannotation>
+ </programlisting>
+ </para>
@@ src/interfaces/libpq/meson.build
'fe-cancel.c',
## src/interfaces/libpq-oauth/Makefile ##
-@@ src/interfaces/libpq-oauth/Makefile: override CFLAGS += $(PTHREAD_CFLAGS)
+@@ src/interfaces/libpq-oauth/Makefile: override CPPFLAGS_SHLIB += -DUSE_PRIVATE_ENCODING_FUNCS
OBJS = \
$(WIN32RES)
@@ src/interfaces/libpq-oauth/Makefile: override CFLAGS += $(PTHREAD_CFLAGS)
oauth-utils.o \
+ fe-auth-oauth-debug_shlib.o
- oauth-utils.o: override CPPFLAGS += -DUSE_DYNAMIC_OAUTH
- oauth-curl_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH
-+fe-auth-oauth-debug_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH
-+
+ oauth-utils.o: override CPPFLAGS += $(CPPFLAGS_SHLIB)
+
+fe-auth-oauth-debug.o: $(libpq_srcdir)/fe-auth-oauth-debug.c
+ $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
+
-+fe-auth-oauth-debug_shlib.o: $(libpq_srcdir)/fe-auth-oauth-debug.c
++fe-auth-oauth-debug_shlib.o: $(libpq_srcdir)/fe-auth-oauth-debug.c fe-auth-oauth-debug.o
+ $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@
-
++
# Add shlib-/stlib-specific objects.
$(shlib): override OBJS += $(OBJS_SHLIB)
+ $(shlib): $(OBJS_SHLIB)
## src/interfaces/libpq/Makefile ##
@@ src/interfaces/libpq/Makefile: OBJS = \
@@ src/interfaces/libpq/Makefile: OBJS = \
ifneq ($(with_ssl),no)
## src/interfaces/libpq-oauth/oauth-utils.h ##
+@@
+ #ifndef OAUTH_UTILS_H
+ #define OAUTH_UTILS_H
+
++#include "fe-auth-oauth.h"
+ #include "libpq-fe.h"
+ #include "pqexpbuffer.h"
+
@@ src/interfaces/libpq-oauth/oauth-utils.h: typedef enum
+ PG_BOOL_NO /* No (false) */
} PGTernaryBool;
- extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3);
-extern bool oauth_unsafe_debugging_enabled(void);
+extern oauth_debug_flags oauth_get_debug_flags(void);
extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
@@ src/interfaces/libpq/fe-auth-oauth.h: typedef struct
+ /* UNSAFE features - require UNSAFE: prefix */
+ bool http; /* allow HTTP (unencrypted) connections */
+ bool trace; /* log HTTP traffic (exposes secrets) */
-+ bool custom_ca; /* allow custom CA certificate file */
+
+ /* SAFE features - allowed without UNSAFE: prefix */
+ bool fast_retry; /* allow zero-second retry intervals */
@@ src/interfaces/libpq/fe-auth-oauth.h: typedef struct
extern void pqClearOAuthToken(PGconn *conn);
-extern bool oauth_unsafe_debugging_enabled(void);
+extern oauth_debug_flags oauth_get_debug_flags(void);
- extern bool use_builtin_flow(PGconn *conn, fe_oauth_state *state);
/* Mechanisms in fe-auth-oauth.c */
+ extern const pg_fe_sasl_mech pg_oauth_mech;
## src/interfaces/libpq-oauth/oauth-curl.c ##
@@ src/interfaces/libpq-oauth/oauth-curl.c: struct async_ctx
@@ src/interfaces/libpq-oauth/oauth-curl.c: setup_curl_handles(struct async_ctx *ac
protos = unsafe;
CHECK_SETOPT(actx, popt, protos, return false);
-@@ src/interfaces/libpq-oauth/oauth-curl.c: setup_curl_handles(struct async_ctx *actx)
- * the flow to work at all, so any changes to the roots are likely to be
- * done system-wide.
- */
-- if (actx->debugging)
-+ if (actx->debug_flags.custom_ca)
- {
- const char *env;
-
@@ src/interfaces/libpq-oauth/oauth-curl.c: check_for_device_flow(struct async_ctx *actx)
* decent time to bail out if we're not using HTTPS for the endpoints
* we'll use for the flow.
@@ src/interfaces/libpq-oauth/oauth-curl.c: check_for_device_flow(struct async_ctx
{
if (pg_strncasecmp(provider->device_authorization_endpoint,
HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
-@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn)
- actx->mux = PGINVALID_SOCKET;
- actx->timerfd = -1;
-
-- /* Should we enable unsafe features? */
-- actx->debugging = oauth_unsafe_debugging_enabled();
-+ /* Parse debug flags from environment */
-+ actx->debug_flags = oauth_get_debug_flags();
-
- state->async_ctx = actx;
-
-@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow(PGconn *conn)
- actx = state->async_ctx;
- Assert(actx || result == PGRES_POLLING_FAILED);
-
-- if (actx && actx->debugging)
+@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow(PGconn *conn, struct PGoauthBearerRequest *request,
+ * drain_timer_events(), when we're in debug mode, track the total number
+ * of calls to this function and print that at the end of the flow.
+ */
+- if (actx->debugging)
+ if (actx && actx->debug_flags.poll_counts)
{
actx->dbg_num_calls++;
if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
+@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request)
+ * Now finish filling in the actx.
+ */
+
+- /* Should we enable unsafe features? */
+- actx->debugging = oauth_unsafe_debugging_enabled();
++ /* Parse debug flags from the environment. */
++ actx->debug_flags = oauth_get_debug_flags();
+
+ initPQExpBuffer(&actx->work_data);
+ initPQExpBuffer(&actx->errbuf);
## src/interfaces/libpq-oauth/oauth-utils.c ##
@@ src/interfaces/libpq-oauth/oauth-utils.c: libpq_gettext(const char *msgid)
@@ src/interfaces/libpq-oauth/test-oauth-curl.c: init_test_actx(void)
- actx->debugging = true;
+ actx->debug_flags.http = true;
+ actx->debug_flags.trace = true;
-+ actx->debug_flags.custom_ca = true;
-+ actx->debug_flags.issuer_mismatch = true;
+ actx->debug_flags.fast_retry = true;
+ actx->debug_flags.poll_counts = true;
+ actx->debug_flags.print_plugin_errors = true;
@@ src/interfaces/libpq/fe-auth-oauth-debug.c (new)
+ *is_unsafe = true;
+ return true;
+ }
-+ else if (strcmp(option, "custom-ca") == 0)
-+ {
-+ flags->custom_ca = true;
-+ *is_unsafe = true;
-+ return true;
-+ }
+ /* Safe options */
+ else if (strcmp(option, "fast-retry") == 0)
+ {
@@ src/interfaces/libpq/fe-auth-oauth-debug.c (new)
+ {
+ flags.http = true;
+ flags.trace = true;
-+ flags.custom_ca = true;
+ flags.fast_retry = true;
+ flags.poll_counts = true;
+ flags.print_plugin_errors = true;
@@ src/interfaces/libpq/fe-auth-oauth.c: issuer_from_well_known_uri(PGconn *conn, c
&& pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
{
/* Allow http:// for testing only. */
-@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state)
+@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
*
* Note that POSIX dlerror() isn't guaranteed to be threadsafe.
*/
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
+ if (oauth_get_debug_flags().print_plugin_errors)
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
- return false;
-@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state)
- * This is more of an error condition than the one above, but due to
- * the dlerror() threadsafety issue, lock it behind PGOAUTHDEBUG too.
+ return 0;
+@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
+ * cause is still locked behind PGOAUTHDEBUG due to the dlerror()
+ * threadsafety issue.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_get_debug_flags().print_plugin_errors)
2: 5fc7a19876b ! 2: 933f6432f87 Add new PGOAUTHDEBUG option: issuer-mismatch
@@ doc/src/sgml/libpq.sgml: PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; e
<term><literal>fast-retry</literal> (safe)</term>
<listitem>
@@ doc/src/sgml/libpq.sgml: PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</linea
+ </para>
<para>
- Unsafe options (<literal>http</literal>, <literal>trace</literal>,
-- <literal>custom-ca</literal>) require the <literal>UNSAFE:</literal> prefix.
-+ <literal>custom-ca</literal>, <literal>issuer-mismatch</literal>) require the <literal>UNSAFE:</literal> prefix.
+- Unsafe options (<literal>http</literal>, <literal>trace</literal>)
+- require the <literal>UNSAFE:</literal> prefix.
++ Unsafe options (<literal>http</literal>, <literal>trace</literal>,
++ <literal>issuer-mismatch</literal>) require the <literal>UNSAFE:</literal> prefix.
If unsafe options are specified without this prefix, a warning is printed
to standard error and that option is ignored. Other valid options in the
list continue to work. Safe options (<literal>fast-retry</literal>,
## src/interfaces/libpq/fe-auth-oauth.h ##
@@ src/interfaces/libpq/fe-auth-oauth.h: typedef struct oauth_debug_flags
+ /* UNSAFE features - require UNSAFE: prefix */
bool http; /* allow HTTP (unencrypted) connections */
bool trace; /* log HTTP traffic (exposes secrets) */
- bool custom_ca; /* allow custom CA certificate file */
+ bool issuer_mismatch; /* tolerate issuer mismatch */
/* SAFE features - allowed without UNSAFE: prefix */
@@ src/interfaces/libpq-oauth/oauth-curl.c: check_issuer(struct async_ctx *actx, PG
return true;
+ ## src/interfaces/libpq-oauth/test-oauth-curl.c ##
+@@ src/interfaces/libpq-oauth/test-oauth-curl.c: init_test_actx(void)
+ actx->timerfd = -1;
+ actx->debug_flags.http = true;
+ actx->debug_flags.trace = true;
++ actx->debug_flags.issuer_mismatch = true;
+ actx->debug_flags.fast_retry = true;
+ actx->debug_flags.poll_counts = true;
+ actx->debug_flags.print_plugin_errors = true;
+
## src/interfaces/libpq/fe-auth-oauth-debug.c ##
@@ src/interfaces/libpq/fe-auth-oauth-debug.c: parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe
*is_unsafe = true;
@@ src/interfaces/libpq/fe-auth-oauth-debug.c: parse_debug_option(const char *optio
else if (strcmp(option, "fast-retry") == 0)
{
@@ src/interfaces/libpq/fe-auth-oauth-debug.c: oauth_get_debug_flags(void)
+ {
flags.http = true;
flags.trace = true;
- flags.custom_ca = true;
+ flags.issuer_mismatch = true;
flags.fast_retry = true;
flags.poll_counts = true;
[application/octet-stream] v2-0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch (19.4K, 3-v2-0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch)
download | inline diff:
From e51f717e07c6374c99c198a3ac18b6df67fb1620 Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <[email protected]>
Date: Thu, 11 Dec 2025 23:56:08 +0000
Subject: [PATCH v2 1/2] Split PGOAUTHDEBUG=UNSAFE into multiple options
---
doc/src/sgml/libpq.sgml | 127 +++++++++++++----
src/interfaces/libpq-oauth/meson.build | 6 +-
src/interfaces/libpq/meson.build | 1 +
src/interfaces/libpq-oauth/Makefile | 11 +-
src/interfaces/libpq/Makefile | 3 +-
src/interfaces/libpq-oauth/oauth-utils.h | 3 +-
src/interfaces/libpq/fe-auth-oauth.h | 18 ++-
src/interfaces/libpq-oauth/oauth-curl.c | 16 +--
src/interfaces/libpq-oauth/oauth-utils.c | 11 --
src/interfaces/libpq-oauth/test-oauth-curl.c | 6 +-
src/interfaces/libpq/fe-auth-oauth-debug.c | 140 +++++++++++++++++++
src/interfaces/libpq/fe-auth-oauth.c | 16 +--
12 files changed, 295 insertions(+), 63 deletions(-)
create mode 100644 src/interfaces/libpq/fe-auth-oauth-debug.c
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index a48d3161495..2e5fb9011e9 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10643,35 +10643,112 @@ typedef struct
</para>
<para>
- A "dangerous debugging mode" may be enabled by setting the environment
- variable <envar>PGOAUTHDEBUG=UNSAFE</envar>. This functionality is provided
- for ease of local development and testing only. It does several things that
- you will not want a production system to do:
+ Debug features may be enabled by setting the <envar>PGOAUTHDEBUG</envar>
+ environment variable. This functionality is provided for ease of local
+ development and testing. The variable accepts a comma-separated list of
+ debug options:
+
+ <programlisting>
+PGOAUTHDEBUG=option1,option2,... <lineannotation>for safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:option1,option2,... <lineannotation>when using unsafe options</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</lineannotation>
+ </programlisting>
+ </para>
- <itemizedlist spacing="compact">
- <listitem>
- <para>
- permits the use of unencrypted HTTP during the OAuth provider exchange
- </para>
- </listitem>
- <listitem>
- <para>
- prints HTTP traffic (containing several critical secrets) to standard
- error during the OAuth flow
- </para>
- </listitem>
- <listitem>
- <para>
- permits the use of zero-second retry intervals, which can cause the
- client to busy-loop and pointlessly consume CPU
- </para>
- </listitem>
- </itemizedlist>
+ <para>
+ Available debug options:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>http</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Permits the use of unencrypted HTTP during the OAuth provider exchange.
+ This allows OAuth credentials to be transmitted over unencrypted
+ connections, which is extremely dangerous and should only be used for
+ local testing.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>trace</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Prints HTTP traffic to standard error during the OAuth flow. This output
+ contains critical secrets including bearer tokens, client secrets, access
+ tokens, and authorization codes. Never share this output with third
+ parties.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>fast-retry</literal> (safe)</term>
+ <listitem>
+ <para>
+ Permits the use of zero-second retry intervals instead of the normal
+ minimum of one second. This can speed up tests but may cause the client
+ to busy-loop and consume CPU unnecessarily.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>poll-counts</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints the total number of poll() calls to standard error when the
+ OAuth flow completes. This helps developers debug the async multiplexer
+ behavior.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>print-plugin-errors</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints plugin loading errors to standard error. This helps developers
+ and package maintainers debug issues when the OAuth plugin fails to load.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+
+ <para>
+ Unsafe options (<literal>http</literal>, <literal>trace</literal>)
+ require the <literal>UNSAFE:</literal> prefix.
+ If unsafe options are specified without this prefix, a warning is printed
+ to standard error and that option is ignored. Other valid options in the
+ list continue to work. Safe options (<literal>fast-retry</literal>,
+ <literal>poll-counts</literal>, <literal>print-plugin-errors</literal>) can
+ be used without the prefix.
</para>
+
+ <para>
+ Unrecognized option names will also trigger a warning and be ignored, while
+ valid options continue to work. This helps catch typos in the environment
+ variable configuration without breaking the debugging of valid options.
+ </para>
+
+ <para>
+ Examples:
+ <programlisting>
+PGOAUTHDEBUG=fast-retry,poll-counts <lineannotation>safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,trace <lineannotation>enable HTTP and traffic logging</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,poll-counts <lineannotation>mix of unsafe and safe</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy; enables all options</lineannotation>
+ </programlisting>
+ </para>
+
<warning>
<para>
- Do not share the output of the OAuth flow traffic with third parties. It
- contains secrets that can be used to attack your clients and servers.
+ Never use unsafe debug options in production environments. The
+ <literal>trace</literal> option in particular exposes secrets that can be
+ used to attack your clients and servers. Do not share the output with third
+ parties.
</para>
</warning>
</sect2>
diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build
index ea3a900f4f1..d8cc92e0c2c 100644
--- a/src/interfaces/libpq-oauth/meson.build
+++ b/src/interfaces/libpq-oauth/meson.build
@@ -6,6 +6,7 @@ endif
libpq_oauth_sources = files(
'oauth-curl.c',
+ '../libpq/fe-auth-oauth-debug.c',
)
# The shared library needs additional glue symbols.
@@ -62,7 +63,10 @@ endif
libpq_oauth_test_deps = []
-oauth_test_sources = files('test-oauth-curl.c') + libpq_oauth_so_sources
+oauth_test_sources = files(
+ 'test-oauth-curl.c',
+ '../libpq/fe-auth-oauth-debug.c',
+) + libpq_oauth_so_sources
if host_system == 'windows'
oauth_test_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index b0ae72167a1..d031f4962e5 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -2,6 +2,7 @@
libpq_sources = files(
'fe-auth-oauth.c',
+ 'fe-auth-oauth-debug.c',
'fe-auth-scram.c',
'fe-auth.c',
'fe-cancel.c',
diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile
index 11e1a3cf528..c6097dda531 100644
--- a/src/interfaces/libpq-oauth/Makefile
+++ b/src/interfaces/libpq-oauth/Makefile
@@ -36,15 +36,24 @@ override CPPFLAGS_SHLIB += -DUSE_PRIVATE_ENCODING_FUNCS
OBJS = \
$(WIN32RES)
-OBJS_STATIC = oauth-curl.o
+OBJS_STATIC = \
+ oauth-curl.o \
+ fe-auth-oauth-debug.o
# The shared library needs additional glue symbols.
OBJS_SHLIB = \
oauth-curl_shlib.o \
oauth-utils.o \
+ fe-auth-oauth-debug_shlib.o
oauth-utils.o: override CPPFLAGS += $(CPPFLAGS_SHLIB)
+fe-auth-oauth-debug.o: $(libpq_srcdir)/fe-auth-oauth-debug.c
+ $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
+
+fe-auth-oauth-debug_shlib.o: $(libpq_srcdir)/fe-auth-oauth-debug.c fe-auth-oauth-debug.o
+ $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@
+
# Add shlib-/stlib-specific objects.
$(shlib): override OBJS += $(OBJS_SHLIB)
$(shlib): $(OBJS_SHLIB)
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 0963995eed4..099c6557e77 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -44,7 +44,8 @@ OBJS = \
legacy-pqsignal.o \
libpq-events.o \
pqexpbuffer.o \
- fe-auth.o
+ fe-auth.o \
+ fe-auth-oauth-debug.o
# File shared across all SSL implementations supported.
ifneq ($(with_ssl),no)
diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h
index 293e9936989..dd4e38d525c 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.h
+++ b/src/interfaces/libpq-oauth/oauth-utils.h
@@ -15,6 +15,7 @@
#ifndef OAUTH_UTILS_H
#define OAUTH_UTILS_H
+#include "fe-auth-oauth.h"
#include "libpq-fe.h"
#include "pqexpbuffer.h"
@@ -35,7 +36,7 @@ typedef enum
PG_BOOL_NO /* No (false) */
} PGTernaryBool;
-extern bool oauth_unsafe_debugging_enabled(void);
+extern oauth_debug_flags oauth_get_debug_flags(void);
extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe);
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 511284614f7..fde5c30c013 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -38,8 +38,24 @@ typedef struct
void *builtin_flow;
} fe_oauth_state;
+/*
+ * Debug flags for PGOAUTHDEBUG environment variable.
+ * Each flag controls a specific debug feature.
+ */
+typedef struct oauth_debug_flags
+{
+ /* UNSAFE features - require UNSAFE: prefix */
+ bool http; /* allow HTTP (unencrypted) connections */
+ bool trace; /* log HTTP traffic (exposes secrets) */
+
+ /* SAFE features - allowed without UNSAFE: prefix */
+ bool fast_retry; /* allow zero-second retry intervals */
+ bool poll_counts; /* print poll() statistics */
+ bool print_plugin_errors; /* print plugin loading errors */
+} oauth_debug_flags;
+
extern void pqClearOAuthToken(PGconn *conn);
-extern bool oauth_unsafe_debugging_enabled(void);
+extern oauth_debug_flags oauth_get_debug_flags(void);
/* Mechanisms in fe-auth-oauth.c */
extern const pg_fe_sasl_mech pg_oauth_mech;
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 3baede1b2e7..564d76cf063 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -274,7 +274,7 @@ struct async_ctx
int running; /* is asynchronous work in progress? */
bool user_prompted; /* have we already sent the authz prompt? */
bool used_basic_auth; /* did we send a client secret? */
- bool debugging; /* can we give unsafe developer assistance? */
+ oauth_debug_flags debug_flags; /* can we give developer assistance */
int dbg_num_calls; /* (debug mode) how many times were we called? */
};
@@ -1023,7 +1023,7 @@ parse_interval(struct async_ctx *actx, const char *interval_str)
parsed = ceil(parsed);
if (parsed < 1)
- return actx->debugging ? 0 : 1;
+ return actx->debug_flags.fast_retry ? 0 : 1;
else if (parsed >= INT_MAX)
return INT_MAX;
@@ -1797,7 +1797,7 @@ setup_curl_handles(struct async_ctx *actx)
*/
CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
- if (actx->debugging)
+ if (actx->debug_flags.trace)
{
/*
* Set a callback for retrieving error information from libcurl, the
@@ -1829,7 +1829,7 @@ setup_curl_handles(struct async_ctx *actx)
const long unsafe = CURLPROTO_HTTPS | CURLPROTO_HTTP;
#endif
- if (actx->debugging)
+ if (actx->debug_flags.http)
protos = unsafe;
CHECK_SETOPT(actx, popt, protos, return false);
@@ -2297,7 +2297,7 @@ check_for_device_flow(struct async_ctx *actx)
* decent time to bail out if we're not using HTTPS for the endpoints
* we'll use for the flow.
*/
- if (!actx->debugging)
+ if (!actx->debug_flags.http)
{
if (pg_strncasecmp(provider->device_authorization_endpoint,
HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
@@ -3027,7 +3027,7 @@ pg_fe_run_oauth_flow(PGconn *conn, struct PGoauthBearerRequest *request,
* drain_timer_events(), when we're in debug mode, track the total number
* of calls to this function and print that at the end of the flow.
*/
- if (actx->debugging)
+ if (actx && actx->debug_flags.poll_counts)
{
actx->dbg_num_calls++;
if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
@@ -3087,8 +3087,8 @@ pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request)
* Now finish filling in the actx.
*/
- /* Should we enable unsafe features? */
- actx->debugging = oauth_unsafe_debugging_enabled();
+ /* Parse debug flags from the environment. */
+ actx->debug_flags = oauth_get_debug_flags();
initPQExpBuffer(&actx->work_data);
initPQExpBuffer(&actx->errbuf);
diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c
index ccb0d9bf2c5..004d41f02aa 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.c
+++ b/src/interfaces/libpq-oauth/oauth-utils.c
@@ -75,17 +75,6 @@ libpq_gettext(const char *msgid)
#endif /* ENABLE_NLS */
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
-
/*
* Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by
* pq_block/reset_sigpipe().
diff --git a/src/interfaces/libpq-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c
index 4328a332738..06815be9a0a 100644
--- a/src/interfaces/libpq-oauth/test-oauth-curl.c
+++ b/src/interfaces/libpq-oauth/test-oauth-curl.c
@@ -89,7 +89,11 @@ init_test_actx(void)
actx->mux = PGINVALID_SOCKET;
actx->timerfd = -1;
- actx->debugging = true;
+ actx->debug_flags.http = true;
+ actx->debug_flags.trace = true;
+ actx->debug_flags.fast_retry = true;
+ actx->debug_flags.poll_counts = true;
+ actx->debug_flags.print_plugin_errors = true;
initPQExpBuffer(&actx->errbuf);
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
new file mode 100644
index 00000000000..f9a1b1f195f
--- /dev/null
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -0,0 +1,140 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-auth-oauth-debug.c
+ * Parsing logic for PGOAUTHDEBUG environment variable
+ *
+ * This file contains pure string parsing logic with no dependencies on
+ * libpq or libpq-oauth implementation details. It's compiled into both
+ * libraries to avoid code duplication.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/fe-auth-oauth-debug.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fe-auth-oauth.h"
+
+/*
+ * Parse a single debug option from PGOAUTHDEBUG.
+ * Returns true if the option is recognized, false otherwise.
+ * Sets *is_unsafe to indicate if this option requires the UNSAFE: prefix.
+ */
+static bool
+parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe)
+{
+ *is_unsafe = false;
+
+ /* Unsafe options */
+ if (strcmp(option, "http") == 0)
+ {
+ flags->http = true;
+ *is_unsafe = true;
+ return true;
+ }
+ else if (strcmp(option, "trace") == 0)
+ {
+ flags->trace = true;
+ *is_unsafe = true;
+ return true;
+ }
+ /* Safe options */
+ else if (strcmp(option, "fast-retry") == 0)
+ {
+ flags->fast_retry = true;
+ return true;
+ }
+ else if (strcmp(option, "poll-counts") == 0)
+ {
+ flags->poll_counts = true;
+ return true;
+ }
+ else if (strcmp(option, "print-plugin-errors") == 0)
+ {
+ flags->print_plugin_errors = true;
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * Parses the PGOAUTHDEBUG environment variable and returns debug flags.
+ *
+ * Supported formats:
+ * PGOAUTHDEBUG=UNSAFE - legacy format, enables all features
+ * PGOAUTHDEBUG=option1,option2 - enable safe features only
+ * PGOAUTHDEBUG=UNSAFE:opt1,opt2 - enable unsafe and/or safe features
+ *
+ * Prints a warning and skips the invalid option if:
+ * - An unrecognized option is specified
+ * - An unsafe option is specified without the UNSAFE: prefix
+ */
+oauth_debug_flags
+oauth_get_debug_flags(void)
+{
+ oauth_debug_flags flags = {0};
+ const char *env = getenv("PGOAUTHDEBUG");
+ char *options_str;
+ char *option;
+ char *saveptr = NULL;
+ bool unsafe_prefix = false;
+
+ if (!env || env[0] == '\0')
+ return flags;
+
+ if (strcmp(env, "UNSAFE") == 0)
+ {
+ flags.http = true;
+ flags.trace = true;
+ flags.fast_retry = true;
+ flags.poll_counts = true;
+ flags.print_plugin_errors = true;
+ return flags;
+ }
+
+ if (strncmp(env, "UNSAFE:", 7) == 0)
+ {
+ unsafe_prefix = true;
+ env += 7;
+ }
+
+ options_str = strdup(env);
+ if (!options_str)
+ return flags;
+
+ option = strtok_r(options_str, ",", &saveptr);
+ while (option != NULL)
+ {
+ bool is_unsafe;
+
+ if (!parse_debug_option(option, &flags, &is_unsafe))
+ {
+ fprintf(stderr,
+ "WARNING: PGOAUTHDEBUG: unrecognized debug option \"%s\" (ignored)\n",
+ option);
+ }
+ else if (is_unsafe && !unsafe_prefix)
+ {
+ fprintf(stderr,
+ "WARNING: PGOAUTHDEBUG: unsafe option \"%s\" requires UNSAFE: prefix (ignored)\n"
+ "Use: PGOAUTHDEBUG=UNSAFE:%s\n",
+ option, option);
+ }
+
+ option = strtok_r(NULL, ",", &saveptr);
+ }
+
+ free(options_str);
+
+ return flags;
+}
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index f93184f04db..4bfe31b03cb 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -383,7 +383,7 @@ issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
authority_start = wkuri + strlen(HTTPS_SCHEME);
if (!authority_start
- && oauth_unsafe_debugging_enabled()
+ && oauth_get_debug_flags().http
&& pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
{
/* Allow http:// for testing only. */
@@ -897,7 +897,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
*
* Note that POSIX dlerror() isn't guaranteed to be threadsafe.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_get_debug_flags().print_plugin_errors)
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
return 0;
@@ -911,7 +911,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
* cause is still locked behind PGOAUTHDEBUG due to the dlerror()
* threadsafety issue.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_get_debug_flags().print_plugin_errors)
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->builtin_flow);
@@ -1418,13 +1418,3 @@ pqClearOAuthToken(PGconn *conn)
conn->oauth_token = NULL;
}
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
--
2.34.1
[application/octet-stream] v2-0002-Add-new-PGOAUTHDEBUG-option-issuer-mismatch.patch (7.9K, 4-v2-0002-Add-new-PGOAUTHDEBUG-option-issuer-mismatch.patch)
download | inline diff:
From 933f6432f8743287977e9da99670c582b91275ae Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <[email protected]>
Date: Wed, 18 Feb 2026 14:51:46 +0100
Subject: [PATCH v2 2/2] Add new PGOAUTHDEBUG option: issuer-mismatch
This new unsafe option allows to connection to proceed if the issuer
configured on the server and client mismatch, allowing to write
mismatched-issuer tests for validators.
Validators should test scenarios like this, as the wire allows this
situation, but previously libpq/psql prevented it, making writing tests
for this more difficult.
---
doc/src/sgml/libpq.sgml | 18 ++++++++++++++++--
src/interfaces/libpq/fe-auth-oauth.h | 1 +
src/interfaces/libpq-oauth/oauth-curl.c | 13 +++++++++----
src/interfaces/libpq-oauth/test-oauth-curl.c | 1 +
src/interfaces/libpq/fe-auth-oauth-debug.c | 7 +++++++
src/interfaces/libpq/fe-auth-oauth.c | 18 +++++++++++-------
.../modules/oauth_validator/t/001_server.pl | 15 +++++++++------
7 files changed, 54 insertions(+), 19 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 2e5fb9011e9..172f8138546 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10683,6 +10683,20 @@ PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</linea
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>issuer-mismatch</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Tolerates a mismatch between the client's configured
+ <literal>oauth_issuer</literal> and the issuer found in the server's
+ discovery document. This disables the mix-up attack protection from
+ RFC 9207 and should only be used in development or testing environments
+ where the server's issuer identifier does not match the client
+ configuration.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>fast-retry</literal> (safe)</term>
<listitem>
@@ -10718,8 +10732,8 @@ PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</linea
</para>
<para>
- Unsafe options (<literal>http</literal>, <literal>trace</literal>)
- require the <literal>UNSAFE:</literal> prefix.
+ Unsafe options (<literal>http</literal>, <literal>trace</literal>,
+ <literal>issuer-mismatch</literal>) require the <literal>UNSAFE:</literal> prefix.
If unsafe options are specified without this prefix, a warning is printed
to standard error and that option is ignored. Other valid options in the
list continue to work. Safe options (<literal>fast-retry</literal>,
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index fde5c30c013..acc678f91cf 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -47,6 +47,7 @@ typedef struct oauth_debug_flags
/* UNSAFE features - require UNSAFE: prefix */
bool http; /* allow HTTP (unencrypted) connections */
bool trace; /* log HTTP traffic (exposes secrets) */
+ bool issuer_mismatch; /* tolerate issuer mismatch */
/* SAFE features - allowed without UNSAFE: prefix */
bool fast_retry; /* allow zero-second retry intervals */
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 564d76cf063..5c18702da62 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -2249,10 +2249,15 @@ check_issuer(struct async_ctx *actx, PGconn *conn)
*/
if (strcmp(oauth_issuer_id, provider->issuer) != 0)
{
- actx_error(actx,
- "the issuer identifier (%s) does not match oauth_issuer (%s)",
- provider->issuer, oauth_issuer_id);
- return false;
+ if (!actx->debug_flags.issuer_mismatch)
+ {
+ actx_error(actx,
+ "the issuer identifier (%s) does not match oauth_issuer (%s)",
+ provider->issuer, oauth_issuer_id);
+ return false;
+ }
+
+ return true;
}
return true;
diff --git a/src/interfaces/libpq-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c
index 06815be9a0a..776eaaafc8d 100644
--- a/src/interfaces/libpq-oauth/test-oauth-curl.c
+++ b/src/interfaces/libpq-oauth/test-oauth-curl.c
@@ -91,6 +91,7 @@ init_test_actx(void)
actx->timerfd = -1;
actx->debug_flags.http = true;
actx->debug_flags.trace = true;
+ actx->debug_flags.issuer_mismatch = true;
actx->debug_flags.fast_retry = true;
actx->debug_flags.poll_counts = true;
actx->debug_flags.print_plugin_errors = true;
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
index f9a1b1f195f..309286e253f 100644
--- a/src/interfaces/libpq/fe-auth-oauth-debug.c
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -47,6 +47,12 @@ parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe
*is_unsafe = true;
return true;
}
+ else if (strcmp(option, "issuer-mismatch") == 0)
+ {
+ flags->issuer_mismatch = true;
+ *is_unsafe = true;
+ return true;
+ }
/* Safe options */
else if (strcmp(option, "fast-retry") == 0)
{
@@ -96,6 +102,7 @@ oauth_get_debug_flags(void)
{
flags.http = true;
flags.trace = true;
+ flags.issuer_mismatch = true;
flags.fast_retry = true;
flags.poll_counts = true;
flags.print_plugin_errors = true;
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 4bfe31b03cb..41340530f3e 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -606,13 +606,16 @@ handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen)
if (strcmp(conn->oauth_issuer_id, discovery_issuer) != 0)
{
- libpq_append_conn_error(conn,
- "server's discovery document at %s (issuer \"%s\") is incompatible with oauth_issuer (%s)",
- ctx.discovery_uri, discovery_issuer,
- conn->oauth_issuer_id);
+ if (!oauth_get_debug_flags().issuer_mismatch)
+ {
+ libpq_append_conn_error(conn,
+ "server's discovery document at %s (issuer \"%s\") is incompatible with oauth_issuer (%s)",
+ ctx.discovery_uri, discovery_issuer,
+ conn->oauth_issuer_id);
- free(discovery_issuer);
- goto cleanup;
+ free(discovery_issuer);
+ goto cleanup;
+ }
}
free(discovery_issuer);
@@ -625,7 +628,8 @@ handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen)
else
{
/* This must match the URI we'd previously determined. */
- if (strcmp(conn->oauth_discovery_uri, ctx.discovery_uri) != 0)
+ if (strcmp(conn->oauth_discovery_uri, ctx.discovery_uri) != 0
+ && !oauth_get_debug_flags().issuer_mismatch)
{
libpq_append_conn_error(conn,
"server's discovery document has moved to %s (previous location was %s)",
diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index 9e4dba8c924..abe47154529 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -190,12 +190,15 @@ $node->connect_ok(
]);
# The issuer linked by the server must match the client's oauth_issuer setting.
-$node->connect_fails(
- "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0636",
- "oauth_issuer must match discovery",
- expected_stderr =>
- qr@server's discovery document at \Q$issuer/.well-known/oauth-authorization-server/alternate\E \(issuer "\Q$issuer/alternate\E"\) is incompatible with oauth_issuer \(\Q$issuer\E\)@
-);
+{
+ local $ENV{PGOAUTHDEBUG} = "UNSAFE:http";
+ $node->connect_fails(
+ "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0636",
+ "oauth_issuer must match discovery",
+ expected_stderr =>
+ qr@server's discovery document at \Q$issuer/.well-known/oauth-authorization-server/alternate\E \(issuer "\Q$issuer/alternate\E"\) is incompatible with oauth_issuer \(\Q$issuer\E\)@
+ );
+}
# Test require_auth settings against OAUTHBEARER.
my @cases = (
--
2.34.1
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-03-30 23:26 Jacob Champion <[email protected]>
parent: Jacob Champion <[email protected]>
0 siblings, 1 reply; 13+ messages in thread
From: Jacob Champion @ 2026-03-30 23:26 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Mon, Mar 30, 2026 at 2:41 PM Jacob Champion
<[email protected]> wrote:
> v2, attached, rebases this over 993368113. The big change is the
> removal of `custom-ca`; there were a couple of other tweaks to get
> both commits compiling independently.
Now for review. 0001:
I like how the UNSAFE: split works in practice. Implementation-wise, I
think I'd prefer that the debug flags be implemented as bits in a
uint32, and then we can cut down on the boilerplate.
fe-auth-oauth-debug.c is IMO too much code for what the feature is
providing. We should ideally be able to include this in a header from
both locations it's used.
I think `fast-retry` needs to be moved under UNSAFE and renamed to
something that doesn't sound "good". `dos-interval` maybe?
nitpick: `poll-counts` and `print-plugin-errors` choose different
naming conventions, and we're not referring to the poll() API for the
former. `call-count`? `dlopen`?
We need to test that ignored unsafe options are in fact ignored (the
parsed flag is currently being accumulated anyway, in contradiction to
the warning message).
I have a sample patch locally for these suggestions, if you'd like.
--
I'm not a fan of 0002; I don't think it provides enough useful
functionality to offset the cost. You said
> validators authors should be able to verify that mismatched
> configurations are rejected properly by the validator.
but "mismatched configuration" is kind of meaningless here: the
validator interacts with a token, not a client. Real-world validator
implementations already need to test against all manner of broken
tokens -- creating one that's signed by the wrong party shouldn't
really be harder to create than one that's signed by the right party
-- and the code to send a custom token from libpq is minimal (<40
lines).
--Jacob
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-03-31 17:44 Zsolt Parragi <[email protected]>
parent: Jacob Champion <[email protected]>
0 siblings, 1 reply; 13+ messages in thread
From: Zsolt Parragi @ 2026-03-31 17:44 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
Thanks for the review, these changes generally sound good.
> I think `fast-retry` needs to be moved under UNSAFE and renamed to
> something that doesn't sound "good". `dos-interval` maybe?
I would use a different name, for something like `dos-interval` I
would expect to provide a time since it's an interval?
`immediate-retry` maybe? Or `dos-retry`?
> nitpick: `poll-counts` and `print-plugin-errors` choose different
> naming conventions, and we're not referring to the poll() API for the
> former. `call-count`? `dlopen`?
I didn't want to write "print-poll-counts" and "print-trace" as those
are just longer, while simply writing "plugin-errors" without print
also seemed wrong. Maybe it could be "plugin-debug" instead, that
sounds good even withour print?
> I have a sample patch locally for these suggestions, if you'd like.
I can create a patch with these updates tomorrow, but if you already
have it, that might be easier/quicker.
> I'm not a fan of 0002
That's okay, I am fine with dropping that. We are already using that
small custom libpq client for testing, so we can keep using it. I just
thought this could make things easier/clearer to others.
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-03-31 23:50 Jacob Champion <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 2 replies; 13+ messages in thread
From: Jacob Champion @ 2026-03-31 23:50 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Tue, Mar 31, 2026 at 10:45 AM Zsolt Parragi
<[email protected]> wrote:
> I didn't want to write "print-poll-counts" and "print-trace" as those
> are just longer, while simply writing "plugin-errors" without print
> also seemed wrong. Maybe it could be "plugin-debug" instead, that
> sounds good even withour print?
I like `plugin-errors`, actually -- "I want to debug these plugin
errors." Adding -debug to the name doesn't seem great to me, since
it's clear from the context that we're debugging.
`dos-retry` still doesn't quite convey what we're doing...
`dos-endpoint` maybe? `busy-loop`? The unsafe aspect is the resource
consumption...
To keep the brainstorming going, I chose the following for v3, but
they're by no means settled:
- http
- trace
- dos-endpoint
- call-count
- plugin-errors
> > I have a sample patch locally for these suggestions, if you'd like.
>
> I can create a patch with these updates tomorrow, but if you already
> have it, that might be easier/quicker.
In the interest of time I've attached it as a single patch, but the
range-diff is rough to read. If you'd like, I can split the code
motion apart from the logical changes tomorrow, to see if it helps
with review.
--Jacob
Attachments:
[application/octet-stream] since-v2.nocfbot.diff (25.6K, 2-since-v2.nocfbot.diff)
download | inline diff:
1: e51f717e07c ! 1: 743b4a5c3a5 Split PGOAUTHDEBUG=UNSAFE into multiple options
@@
## Metadata ##
-Author: Zsolt Parragi <[email protected]>
+Author: Jacob Champion <[email protected]>
## Commit message ##
Split PGOAUTHDEBUG=UNSAFE into multiple options
+ WIP
+
+ Author: Zsolt Parragi <[email protected]>
+ Co-authored-by: Jacob Champion <[email protected]>
+
## doc/src/sgml/libpq.sgml ##
@@ doc/src/sgml/libpq.sgml: typedef struct
</para>
@@ doc/src/sgml/libpq.sgml: typedef struct
+ debug options:
+
+ <programlisting>
-+PGOAUTHDEBUG=option1,option2,... <lineannotation>for safe options only</lineannotation>
-+PGOAUTHDEBUG=UNSAFE:option1,option2,... <lineannotation>when using unsafe options</lineannotation>
-+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</lineannotation>
++PGOAUTHDEBUG=option1,option2,... <lineannotation>for safe options only</lineannotation>
++PGOAUTHDEBUG=UNSAFE:option1,option2,... <lineannotation>when using unsafe options</lineannotation>
++PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</lineannotation>
+ </programlisting>
+ </para>
@@ doc/src/sgml/libpq.sgml: typedef struct
+ </varlistentry>
+
+ <varlistentry>
-+ <term><literal>fast-retry</literal> (safe)</term>
++ <term><literal>dos-endpoint</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Permits the use of zero-second retry intervals instead of the normal
-+ minimum of one second. This can speed up tests but may cause the client
-+ to busy-loop and consume CPU unnecessarily.
++ minimum of one second. This speeds up tests, but in normal operation it
++ will cause the client to busy-loop, consuming CPU and network resources.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
-+ <term><literal>poll-counts</literal> (safe)</term>
++ <term><literal>call-count</literal> (safe)</term>
+ <listitem>
+ <para>
-+ Prints the total number of poll() calls to standard error when the
-+ OAuth flow completes. This helps developers debug the async multiplexer
-+ behavior.
++ Prints the total number of calls to the flow plugin to standard error
++ when the OAuth flow completes. This helps developers debug the async
++ callback behavior.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
-+ <term><literal>print-plugin-errors</literal> (safe)</term>
++ <term><literal>plugin-errors</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints plugin loading errors to standard error. This helps developers
@@ doc/src/sgml/libpq.sgml: typedef struct
+ </para>
+
+ <para>
-+ Unsafe options (<literal>http</literal>, <literal>trace</literal>)
-+ require the <literal>UNSAFE:</literal> prefix.
++ Unsafe options (<literal>http</literal>, <literal>trace</literal>,
++ <literal>dos-endpoint</literal>) require the <literal>UNSAFE:</literal> prefix.
+ If unsafe options are specified without this prefix, a warning is printed
+ to standard error and that option is ignored. Other valid options in the
-+ list continue to work. Safe options (<literal>fast-retry</literal>,
-+ <literal>poll-counts</literal>, <literal>print-plugin-errors</literal>) can
-+ be used without the prefix.
++ list continue to work. Safe options (<literal>call-count</literal>,
++ <literal>plugin-errors</literal>) can be used without the prefix.
</para>
+
+ <para>
@@ doc/src/sgml/libpq.sgml: typedef struct
+ <para>
+ Examples:
+ <programlisting>
-+PGOAUTHDEBUG=fast-retry,poll-counts <lineannotation>safe options only</lineannotation>
-+PGOAUTHDEBUG=UNSAFE:http,trace <lineannotation>enable HTTP and traffic logging</lineannotation>
-+PGOAUTHDEBUG=UNSAFE:http,poll-counts <lineannotation>mix of unsafe and safe</lineannotation>
-+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy; enables all options</lineannotation>
++PGOAUTHDEBUG=call-count <lineannotation>safe options only</lineannotation>
++PGOAUTHDEBUG=UNSAFE:http,trace <lineannotation>enable HTTP and traffic logging</lineannotation>
++PGOAUTHDEBUG=UNSAFE:http,call-count <lineannotation>mix of unsafe and safe</lineannotation>
+ </programlisting>
+ </para>
+
@@ doc/src/sgml/libpq.sgml: typedef struct
<para>
- Do not share the output of the OAuth flow traffic with third parties. It
- contains secrets that can be used to attack your clients and servers.
-+ Never use unsafe debug options in production environments. The
-+ <literal>trace</literal> option in particular exposes secrets that can be
-+ used to attack your clients and servers. Do not share the output with third
-+ parties.
++ Never use unsafe debug options in production environments. They expose
++ secrets and behaviors that can be used to attack your clients and servers.
++ Do not share <literal>trace</literal> output with third parties.
</para>
</warning>
</sect2>
- ## src/interfaces/libpq-oauth/meson.build ##
-@@ src/interfaces/libpq-oauth/meson.build: endif
-
- libpq_oauth_sources = files(
- 'oauth-curl.c',
-+ '../libpq/fe-auth-oauth-debug.c',
- )
-
- # The shared library needs additional glue symbols.
-@@ src/interfaces/libpq-oauth/meson.build: endif
-
- libpq_oauth_test_deps = []
-
--oauth_test_sources = files('test-oauth-curl.c') + libpq_oauth_so_sources
-+oauth_test_sources = files(
-+ 'test-oauth-curl.c',
-+ '../libpq/fe-auth-oauth-debug.c',
-+) + libpq_oauth_so_sources
-
- if host_system == 'windows'
- oauth_test_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
-
- ## src/interfaces/libpq/meson.build ##
-@@
-
- libpq_sources = files(
- 'fe-auth-oauth.c',
-+ 'fe-auth-oauth-debug.c',
- 'fe-auth-scram.c',
- 'fe-auth.c',
- 'fe-cancel.c',
-
- ## src/interfaces/libpq-oauth/Makefile ##
-@@ src/interfaces/libpq-oauth/Makefile: override CPPFLAGS_SHLIB += -DUSE_PRIVATE_ENCODING_FUNCS
- OBJS = \
- $(WIN32RES)
-
--OBJS_STATIC = oauth-curl.o
-+OBJS_STATIC = \
-+ oauth-curl.o \
-+ fe-auth-oauth-debug.o
-
- # The shared library needs additional glue symbols.
- OBJS_SHLIB = \
- oauth-curl_shlib.o \
- oauth-utils.o \
-+ fe-auth-oauth-debug_shlib.o
-
- oauth-utils.o: override CPPFLAGS += $(CPPFLAGS_SHLIB)
-
-+fe-auth-oauth-debug.o: $(libpq_srcdir)/fe-auth-oauth-debug.c
-+ $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
-+
-+fe-auth-oauth-debug_shlib.o: $(libpq_srcdir)/fe-auth-oauth-debug.c fe-auth-oauth-debug.o
-+ $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@
-+
- # Add shlib-/stlib-specific objects.
- $(shlib): override OBJS += $(OBJS_SHLIB)
- $(shlib): $(OBJS_SHLIB)
-
- ## src/interfaces/libpq/Makefile ##
-@@ src/interfaces/libpq/Makefile: OBJS = \
- legacy-pqsignal.o \
- libpq-events.o \
- pqexpbuffer.o \
-- fe-auth.o
-+ fe-auth.o \
-+ fe-auth-oauth-debug.o
-
- # File shared across all SSL implementations supported.
- ifneq ($(with_ssl),no)
-
## src/interfaces/libpq-oauth/oauth-utils.h ##
-@@
- #ifndef OAUTH_UTILS_H
- #define OAUTH_UTILS_H
-
-+#include "fe-auth-oauth.h"
- #include "libpq-fe.h"
- #include "pqexpbuffer.h"
-
@@ src/interfaces/libpq-oauth/oauth-utils.h: typedef enum
PG_BOOL_NO /* No (false) */
} PGTernaryBool;
-extern bool oauth_unsafe_debugging_enabled(void);
-+extern oauth_debug_flags oauth_get_debug_flags(void);
extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe);
## src/interfaces/libpq/fe-auth-oauth.h ##
@@ src/interfaces/libpq/fe-auth-oauth.h: typedef struct
- void *builtin_flow;
} fe_oauth_state;
-+/*
-+ * Debug flags for PGOAUTHDEBUG environment variable.
-+ * Each flag controls a specific debug feature.
-+ */
-+typedef struct oauth_debug_flags
-+{
-+ /* UNSAFE features - require UNSAFE: prefix */
-+ bool http; /* allow HTTP (unencrypted) connections */
-+ bool trace; /* log HTTP traffic (exposes secrets) */
-+
-+ /* SAFE features - allowed without UNSAFE: prefix */
-+ bool fast_retry; /* allow zero-second retry intervals */
-+ bool poll_counts; /* print poll() statistics */
-+ bool print_plugin_errors; /* print plugin loading errors */
-+} oauth_debug_flags;
-+
extern void pqClearOAuthToken(PGconn *conn);
-extern bool oauth_unsafe_debugging_enabled(void);
-+extern oauth_debug_flags oauth_get_debug_flags(void);
/* Mechanisms in fe-auth-oauth.c */
extern const pg_fe_sasl_mech pg_oauth_mech;
+ ## src/interfaces/libpq/oauth-debug.h (new) ##
+@@
++/*-------------------------------------------------------------------------
++ *
++ * oauth-debug.h
++ * Parsing logic for PGOAUTHDEBUG environment variable
++ *
++ * Both libpq and libpq-oauth need this logic, so it's packaged in a small
++ * header for convenience. This is not quite a standalone header, due to the
++ * complication introduced by libpq_gettext(); see note below.
++ *
++ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
++ * Portions Copyright (c) 1994, Regents of the University of California
++ *
++ * IDENTIFICATION
++ * src/interfaces/libpq/oauth-debug.h
++ *
++ *-------------------------------------------------------------------------
++ */
++
++#ifndef OAUTH_DEBUG_H
++#define OAUTH_DEBUG_H
++
++#include "postgres_fe.h"
++
++/*
++ * XXX libpq-oauth can't compile against libpq-int.h, so clients of this header
++ * need to provide the declaration of libpq_gettext() before #including it.
++ * Fortunately, there are only two such clients.
++ */
++/* #include "libpq-int.h" */
++
++/*
++ * Debug flags for the PGOAUTHDEBUG environment variable. Each flag controls a
++ * specific debug feature. OAUTHDEBUG_UNSAFE_* flags require the envvar to have
++ * a literal "UNSAFE:" prefix.
++ */
++
++/* allow HTTP (unencrypted) connections */
++#define OAUTHDEBUG_UNSAFE_HTTP (1<<0)
++/* log HTTP traffic (exposes secrets) */
++#define OAUTHDEBUG_UNSAFE_TRACE (1<<1)
++/* allow zero-second retry intervals */
++#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT (1<<2)
++
++/* mind the gap in values; see OAUTHDEBUG_UNSAFE_MASK below */
++
++/* print PQconnectPoll statistics */
++#define OAUTHDEBUG_CALL_COUNT (1<<16)
++/* print plugin loading errors */
++#define OAUTHDEBUG_PLUGIN_ERRORS (1<<17)
++
++/* all safe and unsafe flags, for the legacy UNSAFE behavior */
++#define OAUTHDEBUG_UNSAFE_ALL ((uint32) ~0)
++
++/* Flags are divided into "safe" and "unsafe" based on bit position. */
++#define OAUTHDEBUG_UNSAFE_MASK ((uint32) 0x0000FFFF)
++
++static_assert(OAUTHDEBUG_CALL_COUNT == OAUTHDEBUG_UNSAFE_MASK + 1,
++ "the first safe OAUTHDEBUG flag should be above OAUTHDEBUG_UNSAFE_MASK");
++
++/*
++ * Parses the PGOAUTHDEBUG environment variable and returns debug flags.
++ *
++ * Supported formats:
++ * PGOAUTHDEBUG=UNSAFE - legacy format, enables all features
++ * PGOAUTHDEBUG=option1,option2 - enable safe features only
++ * PGOAUTHDEBUG=UNSAFE:opt1,opt2 - enable unsafe and/or safe features
++ *
++ * Prints a warning and skips the invalid option if:
++ * - An unrecognized option is specified
++ * - An unsafe option is specified without the UNSAFE: prefix
++ */
++static uint32
++oauth_get_debug_flags(void)
++{
++ uint32 flags = 0;
++ const char *env = getenv("PGOAUTHDEBUG");
++ char *options_str;
++ char *option;
++ char *saveptr = NULL;
++ bool unsafe_allowed = false;
++
++ if (!env || env[0] == '\0')
++ return flags;
++
++ if (strcmp(env, "UNSAFE") == 0)
++ return OAUTHDEBUG_UNSAFE_ALL;
++
++ if (strncmp(env, "UNSAFE:", 7) == 0)
++ {
++ unsafe_allowed = true;
++ env += 7;
++ }
++
++ options_str = strdup(env);
++ if (!options_str)
++ return flags;
++
++ option = strtok_r(options_str, ",", &saveptr);
++ while (option != NULL)
++ {
++ uint32 flag = 0;
++
++ if (strcmp(option, "http") == 0)
++ flag = OAUTHDEBUG_UNSAFE_HTTP;
++ else if (strcmp(option, "trace") == 0)
++ flag = OAUTHDEBUG_UNSAFE_TRACE;
++ else if (strcmp(option, "dos-endpoint") == 0)
++ flag = OAUTHDEBUG_UNSAFE_DOS_ENDPOINT;
++ else if (strcmp(option, "call-count") == 0)
++ flag = OAUTHDEBUG_CALL_COUNT;
++ else if (strcmp(option, "plugin-errors") == 0)
++ flag = OAUTHDEBUG_PLUGIN_ERRORS;
++ else
++ fprintf(stderr,
++ libpq_gettext("WARNING: unrecognized PGOAUTHDEBUG option \"%s\" (ignored)\n"),
++ option);
++
++ if (!unsafe_allowed && ((flag & OAUTHDEBUG_UNSAFE_MASK) != 0))
++ {
++ flag = 0;
++
++ fprintf(stderr,
++ libpq_gettext("WARNING: PGOAUTHDEBUG option \"%s\" is unsafe (ignored)\n"),
++ option);
++ }
++
++ flags |= flag;
++ option = strtok_r(NULL, ",", &saveptr);
++ }
++
++ free(options_str);
++
++ return flags;
++}
++
++#endif /* OAUTH_DEBUG_H */
+
## src/interfaces/libpq-oauth/oauth-curl.c ##
+@@
+
+ #endif /* USE_DYNAMIC_OAUTH */
+
++/*
++ * oauth-debug.h needs the declaration of libpq_gettext(), from one of the above
++ * sources.
++ */
++#include "oauth-debug.h"
++
+ /* One final guardrail against accidental inclusion... */
+ #if defined(USE_DYNAMIC_OAUTH) && defined(LIBPQ_INT_H)
+ #error do not rely on libpq-int.h in dynamic builds of libpq-oauth
@@ src/interfaces/libpq-oauth/oauth-curl.c: struct async_ctx
int running; /* is asynchronous work in progress? */
bool user_prompted; /* have we already sent the authz prompt? */
bool used_basic_auth; /* did we send a client secret? */
- bool debugging; /* can we give unsafe developer assistance? */
-+ oauth_debug_flags debug_flags; /* can we give developer assistance */
++ uint32 debug_flags; /* can we give developer assistance? */
int dbg_num_calls; /* (debug mode) how many times were we called? */
};
@@ src/interfaces/libpq-oauth/oauth-curl.c: parse_interval(struct async_ctx *actx,
if (parsed < 1)
- return actx->debugging ? 0 : 1;
-+ return actx->debug_flags.fast_retry ? 0 : 1;
++ return (actx->debug_flags & OAUTHDEBUG_UNSAFE_DOS_ENDPOINT) ? 0 : 1;
else if (parsed >= INT_MAX)
return INT_MAX;
@@ src/interfaces/libpq-oauth/oauth-curl.c: setup_curl_handles(struct async_ctx *ac
CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
- if (actx->debugging)
-+ if (actx->debug_flags.trace)
++ if (actx->debug_flags & OAUTHDEBUG_UNSAFE_TRACE)
{
/*
* Set a callback for retrieving error information from libcurl, the
@@ src/interfaces/libpq-oauth/oauth-curl.c: setup_curl_handles(struct async_ctx *ac
#endif
- if (actx->debugging)
-+ if (actx->debug_flags.http)
++ if (actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP)
protos = unsafe;
CHECK_SETOPT(actx, popt, protos, return false);
@@ src/interfaces/libpq-oauth/oauth-curl.c: check_for_device_flow(struct async_ctx
* we'll use for the flow.
*/
- if (!actx->debugging)
-+ if (!actx->debug_flags.http)
++ if ((actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP) == 0)
{
if (pg_strncasecmp(provider->device_authorization_endpoint,
HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow(PGconn *conn, stru
* of calls to this function and print that at the end of the flow.
*/
- if (actx->debugging)
-+ if (actx && actx->debug_flags.poll_counts)
++ if (actx->debug_flags & OAUTHDEBUG_CALL_COUNT)
{
actx->dbg_num_calls++;
if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
@@ src/interfaces/libpq-oauth/test-oauth-curl.c: init_test_actx(void)
actx->mux = PGINVALID_SOCKET;
actx->timerfd = -1;
- actx->debugging = true;
-+ actx->debug_flags.http = true;
-+ actx->debug_flags.trace = true;
-+ actx->debug_flags.fast_retry = true;
-+ actx->debug_flags.poll_counts = true;
-+ actx->debug_flags.print_plugin_errors = true;
++ actx->debug_flags = OAUTHDEBUG_UNSAFE_ALL;
initPQExpBuffer(&actx->errbuf);
- ## src/interfaces/libpq/fe-auth-oauth-debug.c (new) ##
-@@
-+/*-------------------------------------------------------------------------
-+ *
-+ * fe-auth-oauth-debug.c
-+ * Parsing logic for PGOAUTHDEBUG environment variable
-+ *
-+ * This file contains pure string parsing logic with no dependencies on
-+ * libpq or libpq-oauth implementation details. It's compiled into both
-+ * libraries to avoid code duplication.
-+ *
-+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
-+ * Portions Copyright (c) 1994, Regents of the University of California
-+ *
-+ * IDENTIFICATION
-+ * src/interfaces/libpq/fe-auth-oauth-debug.c
-+ *
-+ *-------------------------------------------------------------------------
-+ */
-+
-+#include "postgres_fe.h"
-+
-+#include <stdio.h>
-+#include <stdlib.h>
-+#include <string.h>
-+
-+#include "fe-auth-oauth.h"
-+
-+/*
-+ * Parse a single debug option from PGOAUTHDEBUG.
-+ * Returns true if the option is recognized, false otherwise.
-+ * Sets *is_unsafe to indicate if this option requires the UNSAFE: prefix.
-+ */
-+static bool
-+parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe)
-+{
-+ *is_unsafe = false;
-+
-+ /* Unsafe options */
-+ if (strcmp(option, "http") == 0)
-+ {
-+ flags->http = true;
-+ *is_unsafe = true;
-+ return true;
-+ }
-+ else if (strcmp(option, "trace") == 0)
-+ {
-+ flags->trace = true;
-+ *is_unsafe = true;
-+ return true;
-+ }
-+ /* Safe options */
-+ else if (strcmp(option, "fast-retry") == 0)
-+ {
-+ flags->fast_retry = true;
-+ return true;
-+ }
-+ else if (strcmp(option, "poll-counts") == 0)
-+ {
-+ flags->poll_counts = true;
-+ return true;
-+ }
-+ else if (strcmp(option, "print-plugin-errors") == 0)
-+ {
-+ flags->print_plugin_errors = true;
-+ return true;
-+ }
-+
-+ return false;
-+}
-+
-+/*
-+ * Parses the PGOAUTHDEBUG environment variable and returns debug flags.
-+ *
-+ * Supported formats:
-+ * PGOAUTHDEBUG=UNSAFE - legacy format, enables all features
-+ * PGOAUTHDEBUG=option1,option2 - enable safe features only
-+ * PGOAUTHDEBUG=UNSAFE:opt1,opt2 - enable unsafe and/or safe features
-+ *
-+ * Prints a warning and skips the invalid option if:
-+ * - An unrecognized option is specified
-+ * - An unsafe option is specified without the UNSAFE: prefix
-+ */
-+oauth_debug_flags
-+oauth_get_debug_flags(void)
-+{
-+ oauth_debug_flags flags = {0};
-+ const char *env = getenv("PGOAUTHDEBUG");
-+ char *options_str;
-+ char *option;
-+ char *saveptr = NULL;
-+ bool unsafe_prefix = false;
-+
-+ if (!env || env[0] == '\0')
-+ return flags;
-+
-+ if (strcmp(env, "UNSAFE") == 0)
-+ {
-+ flags.http = true;
-+ flags.trace = true;
-+ flags.fast_retry = true;
-+ flags.poll_counts = true;
-+ flags.print_plugin_errors = true;
-+ return flags;
-+ }
-+
-+ if (strncmp(env, "UNSAFE:", 7) == 0)
-+ {
-+ unsafe_prefix = true;
-+ env += 7;
-+ }
-+
-+ options_str = strdup(env);
-+ if (!options_str)
-+ return flags;
-+
-+ option = strtok_r(options_str, ",", &saveptr);
-+ while (option != NULL)
-+ {
-+ bool is_unsafe;
-+
-+ if (!parse_debug_option(option, &flags, &is_unsafe))
-+ {
-+ fprintf(stderr,
-+ "WARNING: PGOAUTHDEBUG: unrecognized debug option \"%s\" (ignored)\n",
-+ option);
-+ }
-+ else if (is_unsafe && !unsafe_prefix)
-+ {
-+ fprintf(stderr,
-+ "WARNING: PGOAUTHDEBUG: unsafe option \"%s\" requires UNSAFE: prefix (ignored)\n"
-+ "Use: PGOAUTHDEBUG=UNSAFE:%s\n",
-+ option, option);
-+ }
-+
-+ option = strtok_r(NULL, ",", &saveptr);
-+ }
-+
-+ free(options_str);
-+
-+ return flags;
-+}
-
## src/interfaces/libpq/fe-auth-oauth.c ##
+@@
+ #include "fe-auth.h"
+ #include "fe-auth-oauth.h"
+ #include "mb/pg_wchar.h"
++#include "oauth-debug.h"
+ #include "pg_config_paths.h"
+ #include "utils/memdebug.h"
+
@@ src/interfaces/libpq/fe-auth-oauth.c: issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
authority_start = wkuri + strlen(HTTPS_SCHEME);
if (!authority_start
- && oauth_unsafe_debugging_enabled()
-+ && oauth_get_debug_flags().http
++ && (oauth_get_debug_flags() & OAUTHDEBUG_UNSAFE_HTTP)
&& pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
{
/* Allow http:// for testing only. */
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
* Note that POSIX dlerror() isn't guaranteed to be threadsafe.
*/
- if (oauth_unsafe_debugging_enabled())
-+ if (oauth_get_debug_flags().print_plugin_errors)
++ if (oauth_get_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
return 0;
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
* threadsafety issue.
*/
- if (oauth_unsafe_debugging_enabled())
-+ if (oauth_get_debug_flags().print_plugin_errors)
++ if (oauth_get_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
- dlclose(state->builtin_flow);
+ dlclose(state->flow_module);
@@ src/interfaces/libpq/fe-auth-oauth.c: pqClearOAuthToken(PGconn *conn)
conn->oauth_token = NULL;
}
@@ src/interfaces/libpq/fe-auth-oauth.c: pqClearOAuthToken(PGconn *conn)
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
+-
+ /*
+ * Hook v1 Poisoning
+ *
+
+ ## src/test/modules/oauth_validator/t/001_server.pl ##
+@@ src/test/modules/oauth_validator/t/001_server.pl: $node->connect_fails(
+ qr@OAuth discovery URI "\Q$issuer\E/.well-known/openid-configuration" must use HTTPS@
+ );
+
++{
++ # PGOAUTHDEBUG=http should have no effect (it needs an UNSAFE: marker).
++ local $ENV{PGOAUTHDEBUG} = "http";
++
++ $node->connect_fails(
++ "user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
++ "HTTPS is required without debug mode (bad PGOAUTHDEBUG value)",
++ expected_stderr => qr[
++ ^WARNING: .* \Qoption "http" is unsafe\E
++ .*
++ \QOAuth discovery URI "$issuer/.well-known/openid-configuration" must use HTTPS\E
++ ]msx
++ );
++}
++
+ # Switch to HTTPS.
+ $issuer = "https://127.0.0.1:$port";
+
+@@ src/test/modules/oauth_validator/t/001_server.pl: $node->connect_ok(
+ ],
+ log_unlike => [qr/FATAL.*OAuth bearer authentication failed/]);
+
+-# Enable PGOAUTHDEBUG for all remaining tests.
+-$ENV{PGOAUTHDEBUG} = "UNSAFE";
++# Enable some debugging features for all remaining tests:
++# - trace, for detailed Curl logs on failure
++# - dos-endpoint, to speed up the three-way handshake
++# - call-count, for our later sanity check
++$ENV{PGOAUTHDEBUG} = "UNSAFE:trace,dos-endpoint,call-count";
+
+ # The /alternate issuer uses slightly different parameters, along with an
+ # OAuth-style discovery document.
+
+ ## src/tools/pginclude/headerscheck ##
+@@ src/tools/pginclude/headerscheck: do
+ test "$f" = src/include/catalog/syscache_ids.h && continue
+ test "$f" = src/include/catalog/syscache_info.h && continue
+
++ test "$f" = src/interfaces/libpq/oauth-debug.h && continue
++
+ # We can't make these Bison output files compilable standalone
+ # without using "%code require", which old Bison versions lack.
+ # parser/gram.h will be included by parser/gramparse.h anyway.
2: 933f6432f87 < -: ----------- Add new PGOAUTHDEBUG option: issuer-mismatch
[application/octet-stream] v3-0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch (19.4K, 3-v3-0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch)
download | inline diff:
From 743b4a5c3a5781e9c7dd5345110ff7b19c1d6707 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Tue, 31 Mar 2026 14:08:59 -0700
Subject: [PATCH v3] Split PGOAUTHDEBUG=UNSAFE into multiple options
WIP
Author: Zsolt Parragi <[email protected]>
Co-authored-by: Jacob Champion <[email protected]>
---
doc/src/sgml/libpq.sgml | 124 ++++++++++++----
src/interfaces/libpq-oauth/oauth-utils.h | 1 -
src/interfaces/libpq/fe-auth-oauth.h | 1 -
src/interfaces/libpq/oauth-debug.h | 136 ++++++++++++++++++
src/interfaces/libpq-oauth/oauth-curl.c | 22 +--
src/interfaces/libpq-oauth/oauth-utils.c | 11 --
src/interfaces/libpq-oauth/test-oauth-curl.c | 2 +-
src/interfaces/libpq/fe-auth-oauth.c | 18 +--
.../modules/oauth_validator/t/001_server.pl | 22 ++-
src/tools/pginclude/headerscheck | 2 +
10 files changed, 276 insertions(+), 63 deletions(-)
create mode 100644 src/interfaces/libpq/oauth-debug.h
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index a48d3161495..01a65419f99 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10643,35 +10643,109 @@ typedef struct
</para>
<para>
- A "dangerous debugging mode" may be enabled by setting the environment
- variable <envar>PGOAUTHDEBUG=UNSAFE</envar>. This functionality is provided
- for ease of local development and testing only. It does several things that
- you will not want a production system to do:
+ Debug features may be enabled by setting the <envar>PGOAUTHDEBUG</envar>
+ environment variable. This functionality is provided for ease of local
+ development and testing. The variable accepts a comma-separated list of
+ debug options:
+
+ <programlisting>
+PGOAUTHDEBUG=option1,option2,... <lineannotation>for safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:option1,option2,... <lineannotation>when using unsafe options</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</lineannotation>
+ </programlisting>
+ </para>
- <itemizedlist spacing="compact">
- <listitem>
- <para>
- permits the use of unencrypted HTTP during the OAuth provider exchange
- </para>
- </listitem>
- <listitem>
- <para>
- prints HTTP traffic (containing several critical secrets) to standard
- error during the OAuth flow
- </para>
- </listitem>
- <listitem>
- <para>
- permits the use of zero-second retry intervals, which can cause the
- client to busy-loop and pointlessly consume CPU
- </para>
- </listitem>
- </itemizedlist>
+ <para>
+ Available debug options:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>http</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Permits the use of unencrypted HTTP during the OAuth provider exchange.
+ This allows OAuth credentials to be transmitted over unencrypted
+ connections, which is extremely dangerous and should only be used for
+ local testing.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>trace</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Prints HTTP traffic to standard error during the OAuth flow. This output
+ contains critical secrets including bearer tokens, client secrets, access
+ tokens, and authorization codes. Never share this output with third
+ parties.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>dos-endpoint</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Permits the use of zero-second retry intervals instead of the normal
+ minimum of one second. This speeds up tests, but in normal operation it
+ will cause the client to busy-loop, consuming CPU and network resources.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>call-count</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints the total number of calls to the flow plugin to standard error
+ when the OAuth flow completes. This helps developers debug the async
+ callback behavior.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>plugin-errors</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints plugin loading errors to standard error. This helps developers
+ and package maintainers debug issues when the OAuth plugin fails to load.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+
+ <para>
+ Unsafe options (<literal>http</literal>, <literal>trace</literal>,
+ <literal>dos-endpoint</literal>) require the <literal>UNSAFE:</literal> prefix.
+ If unsafe options are specified without this prefix, a warning is printed
+ to standard error and that option is ignored. Other valid options in the
+ list continue to work. Safe options (<literal>call-count</literal>,
+ <literal>plugin-errors</literal>) can be used without the prefix.
</para>
+
+ <para>
+ Unrecognized option names will also trigger a warning and be ignored, while
+ valid options continue to work. This helps catch typos in the environment
+ variable configuration without breaking the debugging of valid options.
+ </para>
+
+ <para>
+ Examples:
+ <programlisting>
+PGOAUTHDEBUG=call-count <lineannotation>safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,trace <lineannotation>enable HTTP and traffic logging</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,call-count <lineannotation>mix of unsafe and safe</lineannotation>
+ </programlisting>
+ </para>
+
<warning>
<para>
- Do not share the output of the OAuth flow traffic with third parties. It
- contains secrets that can be used to attack your clients and servers.
+ Never use unsafe debug options in production environments. They expose
+ secrets and behaviors that can be used to attack your clients and servers.
+ Do not share <literal>trace</literal> output with third parties.
</para>
</warning>
</sect2>
diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h
index 293e9936989..dacd2dbacfe 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.h
+++ b/src/interfaces/libpq-oauth/oauth-utils.h
@@ -35,7 +35,6 @@ typedef enum
PG_BOOL_NO /* No (false) */
} PGTernaryBool;
-extern bool oauth_unsafe_debugging_enabled(void);
extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe);
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 872f5df551a..a50d7b03408 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -40,7 +40,6 @@ typedef struct
} fe_oauth_state;
extern void pqClearOAuthToken(PGconn *conn);
-extern bool oauth_unsafe_debugging_enabled(void);
/* Mechanisms in fe-auth-oauth.c */
extern const pg_fe_sasl_mech pg_oauth_mech;
diff --git a/src/interfaces/libpq/oauth-debug.h b/src/interfaces/libpq/oauth-debug.h
new file mode 100644
index 00000000000..0bd8467a09c
--- /dev/null
+++ b/src/interfaces/libpq/oauth-debug.h
@@ -0,0 +1,136 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth-debug.h
+ * Parsing logic for PGOAUTHDEBUG environment variable
+ *
+ * Both libpq and libpq-oauth need this logic, so it's packaged in a small
+ * header for convenience. This is not quite a standalone header, due to the
+ * complication introduced by libpq_gettext(); see note below.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/oauth-debug.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef OAUTH_DEBUG_H
+#define OAUTH_DEBUG_H
+
+#include "postgres_fe.h"
+
+/*
+ * XXX libpq-oauth can't compile against libpq-int.h, so clients of this header
+ * need to provide the declaration of libpq_gettext() before #including it.
+ * Fortunately, there are only two such clients.
+ */
+/* #include "libpq-int.h" */
+
+/*
+ * Debug flags for the PGOAUTHDEBUG environment variable. Each flag controls a
+ * specific debug feature. OAUTHDEBUG_UNSAFE_* flags require the envvar to have
+ * a literal "UNSAFE:" prefix.
+ */
+
+/* allow HTTP (unencrypted) connections */
+#define OAUTHDEBUG_UNSAFE_HTTP (1<<0)
+/* log HTTP traffic (exposes secrets) */
+#define OAUTHDEBUG_UNSAFE_TRACE (1<<1)
+/* allow zero-second retry intervals */
+#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT (1<<2)
+
+/* mind the gap in values; see OAUTHDEBUG_UNSAFE_MASK below */
+
+/* print PQconnectPoll statistics */
+#define OAUTHDEBUG_CALL_COUNT (1<<16)
+/* print plugin loading errors */
+#define OAUTHDEBUG_PLUGIN_ERRORS (1<<17)
+
+/* all safe and unsafe flags, for the legacy UNSAFE behavior */
+#define OAUTHDEBUG_UNSAFE_ALL ((uint32) ~0)
+
+/* Flags are divided into "safe" and "unsafe" based on bit position. */
+#define OAUTHDEBUG_UNSAFE_MASK ((uint32) 0x0000FFFF)
+
+static_assert(OAUTHDEBUG_CALL_COUNT == OAUTHDEBUG_UNSAFE_MASK + 1,
+ "the first safe OAUTHDEBUG flag should be above OAUTHDEBUG_UNSAFE_MASK");
+
+/*
+ * Parses the PGOAUTHDEBUG environment variable and returns debug flags.
+ *
+ * Supported formats:
+ * PGOAUTHDEBUG=UNSAFE - legacy format, enables all features
+ * PGOAUTHDEBUG=option1,option2 - enable safe features only
+ * PGOAUTHDEBUG=UNSAFE:opt1,opt2 - enable unsafe and/or safe features
+ *
+ * Prints a warning and skips the invalid option if:
+ * - An unrecognized option is specified
+ * - An unsafe option is specified without the UNSAFE: prefix
+ */
+static uint32
+oauth_get_debug_flags(void)
+{
+ uint32 flags = 0;
+ const char *env = getenv("PGOAUTHDEBUG");
+ char *options_str;
+ char *option;
+ char *saveptr = NULL;
+ bool unsafe_allowed = false;
+
+ if (!env || env[0] == '\0')
+ return flags;
+
+ if (strcmp(env, "UNSAFE") == 0)
+ return OAUTHDEBUG_UNSAFE_ALL;
+
+ if (strncmp(env, "UNSAFE:", 7) == 0)
+ {
+ unsafe_allowed = true;
+ env += 7;
+ }
+
+ options_str = strdup(env);
+ if (!options_str)
+ return flags;
+
+ option = strtok_r(options_str, ",", &saveptr);
+ while (option != NULL)
+ {
+ uint32 flag = 0;
+
+ if (strcmp(option, "http") == 0)
+ flag = OAUTHDEBUG_UNSAFE_HTTP;
+ else if (strcmp(option, "trace") == 0)
+ flag = OAUTHDEBUG_UNSAFE_TRACE;
+ else if (strcmp(option, "dos-endpoint") == 0)
+ flag = OAUTHDEBUG_UNSAFE_DOS_ENDPOINT;
+ else if (strcmp(option, "call-count") == 0)
+ flag = OAUTHDEBUG_CALL_COUNT;
+ else if (strcmp(option, "plugin-errors") == 0)
+ flag = OAUTHDEBUG_PLUGIN_ERRORS;
+ else
+ fprintf(stderr,
+ libpq_gettext("WARNING: unrecognized PGOAUTHDEBUG option \"%s\" (ignored)\n"),
+ option);
+
+ if (!unsafe_allowed && ((flag & OAUTHDEBUG_UNSAFE_MASK) != 0))
+ {
+ flag = 0;
+
+ fprintf(stderr,
+ libpq_gettext("WARNING: PGOAUTHDEBUG option \"%s\" is unsafe (ignored)\n"),
+ option);
+ }
+
+ flags |= flag;
+ option = strtok_r(NULL, ",", &saveptr);
+ }
+
+ free(options_str);
+
+ return flags;
+}
+
+#endif /* OAUTH_DEBUG_H */
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 3baede1b2e7..eb2fe35d0cc 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -49,6 +49,12 @@
#endif /* USE_DYNAMIC_OAUTH */
+/*
+ * oauth-debug.h needs the declaration of libpq_gettext(), from one of the above
+ * sources.
+ */
+#include "oauth-debug.h"
+
/* One final guardrail against accidental inclusion... */
#if defined(USE_DYNAMIC_OAUTH) && defined(LIBPQ_INT_H)
#error do not rely on libpq-int.h in dynamic builds of libpq-oauth
@@ -274,7 +280,7 @@ struct async_ctx
int running; /* is asynchronous work in progress? */
bool user_prompted; /* have we already sent the authz prompt? */
bool used_basic_auth; /* did we send a client secret? */
- bool debugging; /* can we give unsafe developer assistance? */
+ uint32 debug_flags; /* can we give developer assistance? */
int dbg_num_calls; /* (debug mode) how many times were we called? */
};
@@ -1023,7 +1029,7 @@ parse_interval(struct async_ctx *actx, const char *interval_str)
parsed = ceil(parsed);
if (parsed < 1)
- return actx->debugging ? 0 : 1;
+ return (actx->debug_flags & OAUTHDEBUG_UNSAFE_DOS_ENDPOINT) ? 0 : 1;
else if (parsed >= INT_MAX)
return INT_MAX;
@@ -1797,7 +1803,7 @@ setup_curl_handles(struct async_ctx *actx)
*/
CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
- if (actx->debugging)
+ if (actx->debug_flags & OAUTHDEBUG_UNSAFE_TRACE)
{
/*
* Set a callback for retrieving error information from libcurl, the
@@ -1829,7 +1835,7 @@ setup_curl_handles(struct async_ctx *actx)
const long unsafe = CURLPROTO_HTTPS | CURLPROTO_HTTP;
#endif
- if (actx->debugging)
+ if (actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP)
protos = unsafe;
CHECK_SETOPT(actx, popt, protos, return false);
@@ -2297,7 +2303,7 @@ check_for_device_flow(struct async_ctx *actx)
* decent time to bail out if we're not using HTTPS for the endpoints
* we'll use for the flow.
*/
- if (!actx->debugging)
+ if ((actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP) == 0)
{
if (pg_strncasecmp(provider->device_authorization_endpoint,
HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
@@ -3027,7 +3033,7 @@ pg_fe_run_oauth_flow(PGconn *conn, struct PGoauthBearerRequest *request,
* drain_timer_events(), when we're in debug mode, track the total number
* of calls to this function and print that at the end of the flow.
*/
- if (actx->debugging)
+ if (actx->debug_flags & OAUTHDEBUG_CALL_COUNT)
{
actx->dbg_num_calls++;
if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
@@ -3087,8 +3093,8 @@ pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request)
* Now finish filling in the actx.
*/
- /* Should we enable unsafe features? */
- actx->debugging = oauth_unsafe_debugging_enabled();
+ /* Parse debug flags from the environment. */
+ actx->debug_flags = oauth_get_debug_flags();
initPQExpBuffer(&actx->work_data);
initPQExpBuffer(&actx->errbuf);
diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c
index ccb0d9bf2c5..004d41f02aa 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.c
+++ b/src/interfaces/libpq-oauth/oauth-utils.c
@@ -75,17 +75,6 @@ libpq_gettext(const char *msgid)
#endif /* ENABLE_NLS */
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
-
/*
* Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by
* pq_block/reset_sigpipe().
diff --git a/src/interfaces/libpq-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c
index 4328a332738..185c17e5807 100644
--- a/src/interfaces/libpq-oauth/test-oauth-curl.c
+++ b/src/interfaces/libpq-oauth/test-oauth-curl.c
@@ -89,7 +89,7 @@ init_test_actx(void)
actx->mux = PGINVALID_SOCKET;
actx->timerfd = -1;
- actx->debugging = true;
+ actx->debug_flags = OAUTHDEBUG_UNSAFE_ALL;
initPQExpBuffer(&actx->errbuf);
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index ac03d1d4f9d..c150f27df00 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -26,6 +26,7 @@
#include "fe-auth.h"
#include "fe-auth-oauth.h"
#include "mb/pg_wchar.h"
+#include "oauth-debug.h"
#include "pg_config_paths.h"
#include "utils/memdebug.h"
@@ -389,7 +390,7 @@ issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
authority_start = wkuri + strlen(HTTPS_SCHEME);
if (!authority_start
- && oauth_unsafe_debugging_enabled()
+ && (oauth_get_debug_flags() & OAUTHDEBUG_UNSAFE_HTTP)
&& pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
{
/* Allow http:// for testing only. */
@@ -900,7 +901,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
*
* Note that POSIX dlerror() isn't guaranteed to be threadsafe.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_get_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
return 0;
@@ -922,7 +923,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
* cause is still locked behind PGOAUTHDEBUG due to the dlerror()
* threadsafety issue.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_get_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->flow_module);
@@ -1437,17 +1438,6 @@ pqClearOAuthToken(PGconn *conn)
conn->oauth_token = NULL;
}
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
-
/*
* Hook v1 Poisoning
*
diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index c9c46e63539..3d190c2ba71 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -93,6 +93,21 @@ $node->connect_fails(
qr@OAuth discovery URI "\Q$issuer\E/.well-known/openid-configuration" must use HTTPS@
);
+{
+ # PGOAUTHDEBUG=http should have no effect (it needs an UNSAFE: marker).
+ local $ENV{PGOAUTHDEBUG} = "http";
+
+ $node->connect_fails(
+ "user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
+ "HTTPS is required without debug mode (bad PGOAUTHDEBUG value)",
+ expected_stderr => qr[
+ ^WARNING: .* \Qoption "http" is unsafe\E
+ .*
+ \QOAuth discovery URI "$issuer/.well-known/openid-configuration" must use HTTPS\E
+ ]msx
+ );
+}
+
# Switch to HTTPS.
$issuer = "https://127.0.0.1:$port";
@@ -172,8 +187,11 @@ $node->connect_ok(
],
log_unlike => [qr/FATAL.*OAuth bearer authentication failed/]);
-# Enable PGOAUTHDEBUG for all remaining tests.
-$ENV{PGOAUTHDEBUG} = "UNSAFE";
+# Enable some debugging features for all remaining tests:
+# - trace, for detailed Curl logs on failure
+# - dos-endpoint, to speed up the three-way handshake
+# - call-count, for our later sanity check
+$ENV{PGOAUTHDEBUG} = "UNSAFE:trace,dos-endpoint,call-count";
# The /alternate issuer uses slightly different parameters, along with an
# OAuth-style discovery document.
diff --git a/src/tools/pginclude/headerscheck b/src/tools/pginclude/headerscheck
index 14c466cc237..de50b6937af 100755
--- a/src/tools/pginclude/headerscheck
+++ b/src/tools/pginclude/headerscheck
@@ -153,6 +153,8 @@ do
test "$f" = src/include/catalog/syscache_ids.h && continue
test "$f" = src/include/catalog/syscache_info.h && continue
+ test "$f" = src/interfaces/libpq/oauth-debug.h && continue
+
# We can't make these Bison output files compilable standalone
# without using "%code require", which old Bison versions lack.
# parser/gram.h will be included by parser/gramparse.h anyway.
--
2.34.1
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-04-01 03:45 Chao Li <[email protected]>
parent: Jacob Champion <[email protected]>
1 sibling, 1 reply; 13+ messages in thread
From: Chao Li @ 2026-04-01 03:45 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; PostgreSQL Hackers <[email protected]>
> On Apr 1, 2026, at 07:50, Jacob Champion <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 10:45 AM Zsolt Parragi
> <[email protected]> wrote:
>> I didn't want to write "print-poll-counts" and "print-trace" as those
>> are just longer, while simply writing "plugin-errors" without print
>> also seemed wrong. Maybe it could be "plugin-debug" instead, that
>> sounds good even withour print?
>
> I like `plugin-errors`, actually -- "I want to debug these plugin
> errors." Adding -debug to the name doesn't seem great to me, since
> it's clear from the context that we're debugging.
>
> `dos-retry` still doesn't quite convey what we're doing...
> `dos-endpoint` maybe? `busy-loop`? The unsafe aspect is the resource
> consumption...
>
> To keep the brainstorming going, I chose the following for v3, but
> they're by no means settled:
>
> - http
> - trace
> - dos-endpoint
> - call-count
> - plugin-errors
>
>>> I have a sample patch locally for these suggestions, if you'd like.
>>
>> I can create a patch with these updates tomorrow, but if you already
>> have it, that might be easier/quicker.
>
> In the interest of time I've attached it as a single patch, but the
> range-diff is rough to read. If you'd like, I can split the code
> motion apart from the logical changes tomorrow, to see if it helps
> with review.
>
> --Jacob
> <since-v2.nocfbot.diff><v3-0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch>
Looks like this patch helps reduce uninteresting debug logs. Overall, it looks good to me. I just have a couple of small comments.
1
```
+#define OAUTHDEBUG_UNSAFE_HTTP (1<<0)
```
Since the flags are defined as uint32, does it make sense to define these flag constants as ((uint32) 1 << 0)?
2 oauth_get_debug_flags() reparses PGOAUTHDEBUG every time it is called, which feels a bit unnecessary. But I don't think this is a big deal, since these debug options should never be enabled in production.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-04-01 09:35 Zsolt Parragi <[email protected]>
parent: Jacob Champion <[email protected]>
1 sibling, 1 reply; 13+ messages in thread
From: Zsolt Parragi @ 2026-04-01 09:35 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
> To keep the brainstorming going, I chose the following for v3, but
> they're by no means settled: ...
The names look good. I'm still not 100% sure about plugin-errors, but
since we already have debug in the variable name, it might be okay.
+/* all safe and unsafe flags, for the legacy UNSAFE behavior */
+#define OAUTHDEBUG_UNSAFE_ALL ((uint32) ~0)
The name of this variable is a bit confusing, it's not only about
unsafe settings. I understand why you added the unsafe to it, but
before checking the value/comment I thought this will be the bitmask
of all unsafe options. On the other hand I don't have a better idea
other than simply using ALL.
+oauth_get_debug_flags(void)
+{
+ uint32 flags = 0;
+ const char *env = getenv("PGOAUTHDEBUG");
...
One of the reasons why I implemented this in a C file is because I
wanted to avoid reparsing and warning spam/duplication. Reparsing
shouldn't be a major issue since this is a debug feature, but this
approach causes a warning to print twice in a few corner cases.
That's probably acceptable, the issue is limited in the current code,
but I wanted to mention it as a limitation, as I don't think there's a
good way to fix this without a .c file.
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-04-01 17:05 Jacob Champion <[email protected]>
parent: Chao Li <[email protected]>
0 siblings, 0 replies; 13+ messages in thread
From: Jacob Champion @ 2026-04-01 17:05 UTC (permalink / raw)
To: Chao Li <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; PostgreSQL Hackers <[email protected]>
On Tue, Mar 31, 2026 at 8:45 PM Chao Li <[email protected]> wrote:
> +#define OAUTHDEBUG_UNSAFE_HTTP (1<<0)
>
> Since the flags are defined as uint32, does it make sense to define these flag constants as ((uint32) 1 << 0)?
No, I don't think so. (If we ever got to <<31 we'd need to switch to
1U instead of 1, I think, but I still wouldn't want to write it as a
cast. Bitflags are in pretty wide use across our codebase and I don't
want to introduce a new spelling.)
> 2 oauth_get_debug_flags() reparses PGOAUTHDEBUG every time it is called, which feels a bit unnecessary.
We could maybe rename it oauth_parse_debug_flags(), so it's at least
not hidden/surprising?
> But I don't think this is a big deal, since these debug options should never be enabled in production.
Right.
--Jacob
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-04-01 17:09 Jacob Champion <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 1 reply; 13+ messages in thread
From: Jacob Champion @ 2026-04-01 17:09 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Wed, Apr 1, 2026 at 2:35 AM Zsolt Parragi <[email protected]> wrote:
> +/* all safe and unsafe flags, for the legacy UNSAFE behavior */
> +#define OAUTHDEBUG_UNSAFE_ALL ((uint32) ~0)
>
> The name of this variable is a bit confusing, it's not only about
> unsafe settings. I understand why you added the unsafe to it, but
> before checking the value/comment I thought this will be the bitmask
> of all unsafe options.
I agree.
> On the other hand I don't have a better idea
> other than simply using ALL.
OAUTHDEBUG_LEGACY_UNSAFE?
> +oauth_get_debug_flags(void)
> +{
> + uint32 flags = 0;
> + const char *env = getenv("PGOAUTHDEBUG");
> ...
>
> One of the reasons why I implemented this in a C file is because I
> wanted to avoid reparsing and warning spam/duplication.
I think I'm missing something; how does the choice of .c/.h change
things? There's no static tracking in v1 of the patchset (nor should
there be without locking or atomics, which is not maintenance I really
want to introduce for a debug feature).
> Reparsing
> shouldn't be a major issue since this is a debug feature, but this
> approach causes a warning to print twice in a few corner cases.
Which new corner cases? v1 also prints duplicates (e.g. with
`UNSAFE:blah,http`). I didn't intend to introduce any new calls to
oauth_get_debug_flags() over those already done in v1/v2; if I did
that's a bug.
--Jacob
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-04-01 18:50 Jacob Champion <[email protected]>
parent: Jacob Champion <[email protected]>
0 siblings, 1 reply; 13+ messages in thread
From: Jacob Champion @ 2026-04-01 18:50 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Wed, Apr 1, 2026 at 10:09 AM Jacob Champion
<[email protected]> wrote:
> I didn't intend to introduce any new calls to
> oauth_get_debug_flags() over those already done in v1/v2; if I did
> that's a bug.
To make seeing that a little easier, here's the promised version of v3
as an exploded patch series with more detailed justification, based on
v2-0001.
I'm glad I did that, because I forgot to call attention to a
particular change I made that I think is important:
> fprintf(stderr,
> - "WARNING: PGOAUTHDEBUG: unsafe option \"%s\" requires UNSAFE: prefix (ignored)\n"
> - "Use: PGOAUTHDEBUG=UNSAFE:%s\n",
> - option, option);
> + libpq_gettext("WARNING: PGOAUTHDEBUG option \"%s\" is unsafe (ignored)\n"),
> + option);
`UNSAFE` is intended to be a weak defense against social engineering
attacks. So these warnings need to be translated, if possible, and we
should not provide instructions on how to defeat that defense. The
only people who _should_ be using an unsafe feature should also know
how to fix this problem.
--Jacob
Attachments:
[application/octet-stream] v3.1-0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch (19.5K, 2-v3.1-0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch)
download | inline diff:
From 0b197fbb28720dda6fe36ae483dcd83d2ddb1e40 Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <[email protected]>
Date: Thu, 11 Dec 2025 23:56:08 +0000
Subject: [PATCH v3.1 1/6] Split PGOAUTHDEBUG=UNSAFE into multiple options
---
doc/src/sgml/libpq.sgml | 127 +++++++++++++----
src/interfaces/libpq-oauth/meson.build | 6 +-
src/interfaces/libpq/meson.build | 1 +
src/interfaces/libpq-oauth/Makefile | 11 +-
src/interfaces/libpq/Makefile | 3 +-
src/interfaces/libpq-oauth/oauth-utils.h | 3 +-
src/interfaces/libpq/fe-auth-oauth.h | 18 ++-
src/interfaces/libpq-oauth/oauth-curl.c | 16 +--
src/interfaces/libpq-oauth/oauth-utils.c | 11 --
src/interfaces/libpq-oauth/test-oauth-curl.c | 6 +-
src/interfaces/libpq/fe-auth-oauth-debug.c | 140 +++++++++++++++++++
src/interfaces/libpq/fe-auth-oauth.c | 17 +--
12 files changed, 295 insertions(+), 64 deletions(-)
create mode 100644 src/interfaces/libpq/fe-auth-oauth-debug.c
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index a48d3161495..2e5fb9011e9 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10643,35 +10643,112 @@ typedef struct
</para>
<para>
- A "dangerous debugging mode" may be enabled by setting the environment
- variable <envar>PGOAUTHDEBUG=UNSAFE</envar>. This functionality is provided
- for ease of local development and testing only. It does several things that
- you will not want a production system to do:
+ Debug features may be enabled by setting the <envar>PGOAUTHDEBUG</envar>
+ environment variable. This functionality is provided for ease of local
+ development and testing. The variable accepts a comma-separated list of
+ debug options:
+
+ <programlisting>
+PGOAUTHDEBUG=option1,option2,... <lineannotation>for safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:option1,option2,... <lineannotation>when using unsafe options</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</lineannotation>
+ </programlisting>
+ </para>
- <itemizedlist spacing="compact">
- <listitem>
- <para>
- permits the use of unencrypted HTTP during the OAuth provider exchange
- </para>
- </listitem>
- <listitem>
- <para>
- prints HTTP traffic (containing several critical secrets) to standard
- error during the OAuth flow
- </para>
- </listitem>
- <listitem>
- <para>
- permits the use of zero-second retry intervals, which can cause the
- client to busy-loop and pointlessly consume CPU
- </para>
- </listitem>
- </itemizedlist>
+ <para>
+ Available debug options:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>http</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Permits the use of unencrypted HTTP during the OAuth provider exchange.
+ This allows OAuth credentials to be transmitted over unencrypted
+ connections, which is extremely dangerous and should only be used for
+ local testing.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>trace</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Prints HTTP traffic to standard error during the OAuth flow. This output
+ contains critical secrets including bearer tokens, client secrets, access
+ tokens, and authorization codes. Never share this output with third
+ parties.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>fast-retry</literal> (safe)</term>
+ <listitem>
+ <para>
+ Permits the use of zero-second retry intervals instead of the normal
+ minimum of one second. This can speed up tests but may cause the client
+ to busy-loop and consume CPU unnecessarily.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>poll-counts</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints the total number of poll() calls to standard error when the
+ OAuth flow completes. This helps developers debug the async multiplexer
+ behavior.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>print-plugin-errors</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints plugin loading errors to standard error. This helps developers
+ and package maintainers debug issues when the OAuth plugin fails to load.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+
+ <para>
+ Unsafe options (<literal>http</literal>, <literal>trace</literal>)
+ require the <literal>UNSAFE:</literal> prefix.
+ If unsafe options are specified without this prefix, a warning is printed
+ to standard error and that option is ignored. Other valid options in the
+ list continue to work. Safe options (<literal>fast-retry</literal>,
+ <literal>poll-counts</literal>, <literal>print-plugin-errors</literal>) can
+ be used without the prefix.
</para>
+
+ <para>
+ Unrecognized option names will also trigger a warning and be ignored, while
+ valid options continue to work. This helps catch typos in the environment
+ variable configuration without breaking the debugging of valid options.
+ </para>
+
+ <para>
+ Examples:
+ <programlisting>
+PGOAUTHDEBUG=fast-retry,poll-counts <lineannotation>safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,trace <lineannotation>enable HTTP and traffic logging</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,poll-counts <lineannotation>mix of unsafe and safe</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy; enables all options</lineannotation>
+ </programlisting>
+ </para>
+
<warning>
<para>
- Do not share the output of the OAuth flow traffic with third parties. It
- contains secrets that can be used to attack your clients and servers.
+ Never use unsafe debug options in production environments. The
+ <literal>trace</literal> option in particular exposes secrets that can be
+ used to attack your clients and servers. Do not share the output with third
+ parties.
</para>
</warning>
</sect2>
diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build
index ea3a900f4f1..d8cc92e0c2c 100644
--- a/src/interfaces/libpq-oauth/meson.build
+++ b/src/interfaces/libpq-oauth/meson.build
@@ -6,6 +6,7 @@ endif
libpq_oauth_sources = files(
'oauth-curl.c',
+ '../libpq/fe-auth-oauth-debug.c',
)
# The shared library needs additional glue symbols.
@@ -62,7 +63,10 @@ endif
libpq_oauth_test_deps = []
-oauth_test_sources = files('test-oauth-curl.c') + libpq_oauth_so_sources
+oauth_test_sources = files(
+ 'test-oauth-curl.c',
+ '../libpq/fe-auth-oauth-debug.c',
+) + libpq_oauth_so_sources
if host_system == 'windows'
oauth_test_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index b0ae72167a1..d031f4962e5 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -2,6 +2,7 @@
libpq_sources = files(
'fe-auth-oauth.c',
+ 'fe-auth-oauth-debug.c',
'fe-auth-scram.c',
'fe-auth.c',
'fe-cancel.c',
diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile
index 11e1a3cf528..c6097dda531 100644
--- a/src/interfaces/libpq-oauth/Makefile
+++ b/src/interfaces/libpq-oauth/Makefile
@@ -36,15 +36,24 @@ override CPPFLAGS_SHLIB += -DUSE_PRIVATE_ENCODING_FUNCS
OBJS = \
$(WIN32RES)
-OBJS_STATIC = oauth-curl.o
+OBJS_STATIC = \
+ oauth-curl.o \
+ fe-auth-oauth-debug.o
# The shared library needs additional glue symbols.
OBJS_SHLIB = \
oauth-curl_shlib.o \
oauth-utils.o \
+ fe-auth-oauth-debug_shlib.o
oauth-utils.o: override CPPFLAGS += $(CPPFLAGS_SHLIB)
+fe-auth-oauth-debug.o: $(libpq_srcdir)/fe-auth-oauth-debug.c
+ $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
+
+fe-auth-oauth-debug_shlib.o: $(libpq_srcdir)/fe-auth-oauth-debug.c fe-auth-oauth-debug.o
+ $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@
+
# Add shlib-/stlib-specific objects.
$(shlib): override OBJS += $(OBJS_SHLIB)
$(shlib): $(OBJS_SHLIB)
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 0963995eed4..099c6557e77 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -44,7 +44,8 @@ OBJS = \
legacy-pqsignal.o \
libpq-events.o \
pqexpbuffer.o \
- fe-auth.o
+ fe-auth.o \
+ fe-auth-oauth-debug.o
# File shared across all SSL implementations supported.
ifneq ($(with_ssl),no)
diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h
index 293e9936989..dd4e38d525c 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.h
+++ b/src/interfaces/libpq-oauth/oauth-utils.h
@@ -15,6 +15,7 @@
#ifndef OAUTH_UTILS_H
#define OAUTH_UTILS_H
+#include "fe-auth-oauth.h"
#include "libpq-fe.h"
#include "pqexpbuffer.h"
@@ -35,7 +36,7 @@ typedef enum
PG_BOOL_NO /* No (false) */
} PGTernaryBool;
-extern bool oauth_unsafe_debugging_enabled(void);
+extern oauth_debug_flags oauth_get_debug_flags(void);
extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe);
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 872f5df551a..859975bcd67 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -39,8 +39,24 @@ typedef struct
void *flow_module;
} fe_oauth_state;
+/*
+ * Debug flags for PGOAUTHDEBUG environment variable.
+ * Each flag controls a specific debug feature.
+ */
+typedef struct oauth_debug_flags
+{
+ /* UNSAFE features - require UNSAFE: prefix */
+ bool http; /* allow HTTP (unencrypted) connections */
+ bool trace; /* log HTTP traffic (exposes secrets) */
+
+ /* SAFE features - allowed without UNSAFE: prefix */
+ bool fast_retry; /* allow zero-second retry intervals */
+ bool poll_counts; /* print poll() statistics */
+ bool print_plugin_errors; /* print plugin loading errors */
+} oauth_debug_flags;
+
extern void pqClearOAuthToken(PGconn *conn);
-extern bool oauth_unsafe_debugging_enabled(void);
+extern oauth_debug_flags oauth_get_debug_flags(void);
/* Mechanisms in fe-auth-oauth.c */
extern const pg_fe_sasl_mech pg_oauth_mech;
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 3baede1b2e7..564d76cf063 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -274,7 +274,7 @@ struct async_ctx
int running; /* is asynchronous work in progress? */
bool user_prompted; /* have we already sent the authz prompt? */
bool used_basic_auth; /* did we send a client secret? */
- bool debugging; /* can we give unsafe developer assistance? */
+ oauth_debug_flags debug_flags; /* can we give developer assistance */
int dbg_num_calls; /* (debug mode) how many times were we called? */
};
@@ -1023,7 +1023,7 @@ parse_interval(struct async_ctx *actx, const char *interval_str)
parsed = ceil(parsed);
if (parsed < 1)
- return actx->debugging ? 0 : 1;
+ return actx->debug_flags.fast_retry ? 0 : 1;
else if (parsed >= INT_MAX)
return INT_MAX;
@@ -1797,7 +1797,7 @@ setup_curl_handles(struct async_ctx *actx)
*/
CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
- if (actx->debugging)
+ if (actx->debug_flags.trace)
{
/*
* Set a callback for retrieving error information from libcurl, the
@@ -1829,7 +1829,7 @@ setup_curl_handles(struct async_ctx *actx)
const long unsafe = CURLPROTO_HTTPS | CURLPROTO_HTTP;
#endif
- if (actx->debugging)
+ if (actx->debug_flags.http)
protos = unsafe;
CHECK_SETOPT(actx, popt, protos, return false);
@@ -2297,7 +2297,7 @@ check_for_device_flow(struct async_ctx *actx)
* decent time to bail out if we're not using HTTPS for the endpoints
* we'll use for the flow.
*/
- if (!actx->debugging)
+ if (!actx->debug_flags.http)
{
if (pg_strncasecmp(provider->device_authorization_endpoint,
HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
@@ -3027,7 +3027,7 @@ pg_fe_run_oauth_flow(PGconn *conn, struct PGoauthBearerRequest *request,
* drain_timer_events(), when we're in debug mode, track the total number
* of calls to this function and print that at the end of the flow.
*/
- if (actx->debugging)
+ if (actx && actx->debug_flags.poll_counts)
{
actx->dbg_num_calls++;
if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
@@ -3087,8 +3087,8 @@ pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request)
* Now finish filling in the actx.
*/
- /* Should we enable unsafe features? */
- actx->debugging = oauth_unsafe_debugging_enabled();
+ /* Parse debug flags from the environment. */
+ actx->debug_flags = oauth_get_debug_flags();
initPQExpBuffer(&actx->work_data);
initPQExpBuffer(&actx->errbuf);
diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c
index ccb0d9bf2c5..004d41f02aa 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.c
+++ b/src/interfaces/libpq-oauth/oauth-utils.c
@@ -75,17 +75,6 @@ libpq_gettext(const char *msgid)
#endif /* ENABLE_NLS */
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
-
/*
* Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by
* pq_block/reset_sigpipe().
diff --git a/src/interfaces/libpq-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c
index 4328a332738..06815be9a0a 100644
--- a/src/interfaces/libpq-oauth/test-oauth-curl.c
+++ b/src/interfaces/libpq-oauth/test-oauth-curl.c
@@ -89,7 +89,11 @@ init_test_actx(void)
actx->mux = PGINVALID_SOCKET;
actx->timerfd = -1;
- actx->debugging = true;
+ actx->debug_flags.http = true;
+ actx->debug_flags.trace = true;
+ actx->debug_flags.fast_retry = true;
+ actx->debug_flags.poll_counts = true;
+ actx->debug_flags.print_plugin_errors = true;
initPQExpBuffer(&actx->errbuf);
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
new file mode 100644
index 00000000000..f9a1b1f195f
--- /dev/null
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -0,0 +1,140 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-auth-oauth-debug.c
+ * Parsing logic for PGOAUTHDEBUG environment variable
+ *
+ * This file contains pure string parsing logic with no dependencies on
+ * libpq or libpq-oauth implementation details. It's compiled into both
+ * libraries to avoid code duplication.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/fe-auth-oauth-debug.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fe-auth-oauth.h"
+
+/*
+ * Parse a single debug option from PGOAUTHDEBUG.
+ * Returns true if the option is recognized, false otherwise.
+ * Sets *is_unsafe to indicate if this option requires the UNSAFE: prefix.
+ */
+static bool
+parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe)
+{
+ *is_unsafe = false;
+
+ /* Unsafe options */
+ if (strcmp(option, "http") == 0)
+ {
+ flags->http = true;
+ *is_unsafe = true;
+ return true;
+ }
+ else if (strcmp(option, "trace") == 0)
+ {
+ flags->trace = true;
+ *is_unsafe = true;
+ return true;
+ }
+ /* Safe options */
+ else if (strcmp(option, "fast-retry") == 0)
+ {
+ flags->fast_retry = true;
+ return true;
+ }
+ else if (strcmp(option, "poll-counts") == 0)
+ {
+ flags->poll_counts = true;
+ return true;
+ }
+ else if (strcmp(option, "print-plugin-errors") == 0)
+ {
+ flags->print_plugin_errors = true;
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * Parses the PGOAUTHDEBUG environment variable and returns debug flags.
+ *
+ * Supported formats:
+ * PGOAUTHDEBUG=UNSAFE - legacy format, enables all features
+ * PGOAUTHDEBUG=option1,option2 - enable safe features only
+ * PGOAUTHDEBUG=UNSAFE:opt1,opt2 - enable unsafe and/or safe features
+ *
+ * Prints a warning and skips the invalid option if:
+ * - An unrecognized option is specified
+ * - An unsafe option is specified without the UNSAFE: prefix
+ */
+oauth_debug_flags
+oauth_get_debug_flags(void)
+{
+ oauth_debug_flags flags = {0};
+ const char *env = getenv("PGOAUTHDEBUG");
+ char *options_str;
+ char *option;
+ char *saveptr = NULL;
+ bool unsafe_prefix = false;
+
+ if (!env || env[0] == '\0')
+ return flags;
+
+ if (strcmp(env, "UNSAFE") == 0)
+ {
+ flags.http = true;
+ flags.trace = true;
+ flags.fast_retry = true;
+ flags.poll_counts = true;
+ flags.print_plugin_errors = true;
+ return flags;
+ }
+
+ if (strncmp(env, "UNSAFE:", 7) == 0)
+ {
+ unsafe_prefix = true;
+ env += 7;
+ }
+
+ options_str = strdup(env);
+ if (!options_str)
+ return flags;
+
+ option = strtok_r(options_str, ",", &saveptr);
+ while (option != NULL)
+ {
+ bool is_unsafe;
+
+ if (!parse_debug_option(option, &flags, &is_unsafe))
+ {
+ fprintf(stderr,
+ "WARNING: PGOAUTHDEBUG: unrecognized debug option \"%s\" (ignored)\n",
+ option);
+ }
+ else if (is_unsafe && !unsafe_prefix)
+ {
+ fprintf(stderr,
+ "WARNING: PGOAUTHDEBUG: unsafe option \"%s\" requires UNSAFE: prefix (ignored)\n"
+ "Use: PGOAUTHDEBUG=UNSAFE:%s\n",
+ option, option);
+ }
+
+ option = strtok_r(NULL, ",", &saveptr);
+ }
+
+ free(options_str);
+
+ return flags;
+}
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index ac03d1d4f9d..5f5900a9ae7 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -389,7 +389,7 @@ issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
authority_start = wkuri + strlen(HTTPS_SCHEME);
if (!authority_start
- && oauth_unsafe_debugging_enabled()
+ && oauth_get_debug_flags().http
&& pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
{
/* Allow http:// for testing only. */
@@ -900,7 +900,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
*
* Note that POSIX dlerror() isn't guaranteed to be threadsafe.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_get_debug_flags().print_plugin_errors)
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
return 0;
@@ -922,7 +922,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
* cause is still locked behind PGOAUTHDEBUG due to the dlerror()
* threadsafety issue.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_get_debug_flags().print_plugin_errors)
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->flow_module);
@@ -1437,17 +1437,6 @@ pqClearOAuthToken(PGconn *conn)
conn->oauth_token = NULL;
}
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
-
/*
* Hook v1 Poisoning
*
--
2.34.1
[application/octet-stream] v3.1-0002-squash-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-.patch (6.1K, 3-v3.1-0002-squash-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-.patch)
download | inline diff:
From 6219a268df4fefdd99bf64a3c2de9548338cbc62 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Wed, 1 Apr 2026 10:29:16 -0700
Subject: [PATCH v3.1 2/6] squash! Split PGOAUTHDEBUG=UNSAFE into multiple
options
- Rename user-facing options in parsing and documentation.
- Move dos-endpoint to UNSAFE.
- Realign lineannotations and update doc descriptions.
---
doc/src/sgml/libpq.sgml | 45 ++++++++++------------
src/interfaces/libpq/fe-auth-oauth-debug.c | 9 +++--
2 files changed, 26 insertions(+), 28 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 2e5fb9011e9..01a65419f99 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10649,9 +10649,9 @@ typedef struct
debug options:
<programlisting>
-PGOAUTHDEBUG=option1,option2,... <lineannotation>for safe options only</lineannotation>
-PGOAUTHDEBUG=UNSAFE:option1,option2,... <lineannotation>when using unsafe options</lineannotation>
-PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</lineannotation>
+PGOAUTHDEBUG=option1,option2,... <lineannotation>for safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:option1,option2,... <lineannotation>when using unsafe options</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</lineannotation>
</programlisting>
</para>
@@ -10684,29 +10684,29 @@ PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</linea
</varlistentry>
<varlistentry>
- <term><literal>fast-retry</literal> (safe)</term>
+ <term><literal>dos-endpoint</literal> (unsafe)</term>
<listitem>
<para>
Permits the use of zero-second retry intervals instead of the normal
- minimum of one second. This can speed up tests but may cause the client
- to busy-loop and consume CPU unnecessarily.
+ minimum of one second. This speeds up tests, but in normal operation it
+ will cause the client to busy-loop, consuming CPU and network resources.
</para>
</listitem>
</varlistentry>
<varlistentry>
- <term><literal>poll-counts</literal> (safe)</term>
+ <term><literal>call-count</literal> (safe)</term>
<listitem>
<para>
- Prints the total number of poll() calls to standard error when the
- OAuth flow completes. This helps developers debug the async multiplexer
- behavior.
+ Prints the total number of calls to the flow plugin to standard error
+ when the OAuth flow completes. This helps developers debug the async
+ callback behavior.
</para>
</listitem>
</varlistentry>
<varlistentry>
- <term><literal>print-plugin-errors</literal> (safe)</term>
+ <term><literal>plugin-errors</literal> (safe)</term>
<listitem>
<para>
Prints plugin loading errors to standard error. This helps developers
@@ -10718,13 +10718,12 @@ PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</linea
</para>
<para>
- Unsafe options (<literal>http</literal>, <literal>trace</literal>)
- require the <literal>UNSAFE:</literal> prefix.
+ Unsafe options (<literal>http</literal>, <literal>trace</literal>,
+ <literal>dos-endpoint</literal>) require the <literal>UNSAFE:</literal> prefix.
If unsafe options are specified without this prefix, a warning is printed
to standard error and that option is ignored. Other valid options in the
- list continue to work. Safe options (<literal>fast-retry</literal>,
- <literal>poll-counts</literal>, <literal>print-plugin-errors</literal>) can
- be used without the prefix.
+ list continue to work. Safe options (<literal>call-count</literal>,
+ <literal>plugin-errors</literal>) can be used without the prefix.
</para>
<para>
@@ -10736,19 +10735,17 @@ PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</linea
<para>
Examples:
<programlisting>
-PGOAUTHDEBUG=fast-retry,poll-counts <lineannotation>safe options only</lineannotation>
-PGOAUTHDEBUG=UNSAFE:http,trace <lineannotation>enable HTTP and traffic logging</lineannotation>
-PGOAUTHDEBUG=UNSAFE:http,poll-counts <lineannotation>mix of unsafe and safe</lineannotation>
-PGOAUTHDEBUG=UNSAFE <lineannotation>legacy; enables all options</lineannotation>
+PGOAUTHDEBUG=call-count <lineannotation>safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,trace <lineannotation>enable HTTP and traffic logging</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,call-count <lineannotation>mix of unsafe and safe</lineannotation>
</programlisting>
</para>
<warning>
<para>
- Never use unsafe debug options in production environments. The
- <literal>trace</literal> option in particular exposes secrets that can be
- used to attack your clients and servers. Do not share the output with third
- parties.
+ Never use unsafe debug options in production environments. They expose
+ secrets and behaviors that can be used to attack your clients and servers.
+ Do not share <literal>trace</literal> output with third parties.
</para>
</warning>
</sect2>
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
index f9a1b1f195f..957da5d4068 100644
--- a/src/interfaces/libpq/fe-auth-oauth-debug.c
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -47,18 +47,19 @@ parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe
*is_unsafe = true;
return true;
}
- /* Safe options */
- else if (strcmp(option, "fast-retry") == 0)
+ else if (strcmp(option, "dos-endpoint") == 0)
{
flags->fast_retry = true;
+ *is_unsafe = true;
return true;
}
- else if (strcmp(option, "poll-counts") == 0)
+ /* Safe options */
+ else if (strcmp(option, "call-count") == 0)
{
flags->poll_counts = true;
return true;
}
- else if (strcmp(option, "print-plugin-errors") == 0)
+ else if (strcmp(option, "plugin-errors") == 0)
{
flags->print_plugin_errors = true;
return true;
--
2.34.1
[application/octet-stream] v3.1-0003-squash-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-.patch (9.7K, 4-v3.1-0003-squash-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-.patch)
download | inline diff:
From fb26bb8b21e2c873d5c4cfd8fd54be692e67aaff Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Wed, 1 Apr 2026 10:58:46 -0700
Subject: [PATCH v3.1 3/6] squash! Split PGOAUTHDEBUG=UNSAFE into multiple
options
- Switch from a struct to bitflags. (OAUTHDEBUG_UNSAFE_MASK is
introduced a commit early to avoid unnecessary work.)
---
src/interfaces/libpq-oauth/oauth-utils.h | 2 +-
src/interfaces/libpq/fe-auth-oauth.h | 39 +++++++++++++-------
src/interfaces/libpq-oauth/oauth-curl.c | 12 +++---
src/interfaces/libpq-oauth/test-oauth-curl.c | 6 +--
src/interfaces/libpq/fe-auth-oauth-debug.c | 25 +++++--------
src/interfaces/libpq/fe-auth-oauth.c | 6 +--
6 files changed, 46 insertions(+), 44 deletions(-)
diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h
index dd4e38d525c..64a9235ee85 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.h
+++ b/src/interfaces/libpq-oauth/oauth-utils.h
@@ -36,7 +36,7 @@ typedef enum
PG_BOOL_NO /* No (false) */
} PGTernaryBool;
-extern oauth_debug_flags oauth_get_debug_flags(void);
+extern uint32 oauth_get_debug_flags(void);
extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe);
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 859975bcd67..a952fea09cf 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -40,23 +40,36 @@ typedef struct
} fe_oauth_state;
/*
- * Debug flags for PGOAUTHDEBUG environment variable.
- * Each flag controls a specific debug feature.
+ * Debug flags for the PGOAUTHDEBUG environment variable. Each flag controls a
+ * specific debug feature. OAUTHDEBUG_UNSAFE_* flags require the envvar to have
+ * a literal "UNSAFE:" prefix.
*/
-typedef struct oauth_debug_flags
-{
- /* UNSAFE features - require UNSAFE: prefix */
- bool http; /* allow HTTP (unencrypted) connections */
- bool trace; /* log HTTP traffic (exposes secrets) */
- /* SAFE features - allowed without UNSAFE: prefix */
- bool fast_retry; /* allow zero-second retry intervals */
- bool poll_counts; /* print poll() statistics */
- bool print_plugin_errors; /* print plugin loading errors */
-} oauth_debug_flags;
+/* allow HTTP (unencrypted) connections */
+#define OAUTHDEBUG_UNSAFE_HTTP (1<<0)
+/* log HTTP traffic (exposes secrets) */
+#define OAUTHDEBUG_UNSAFE_TRACE (1<<1)
+/* allow zero-second retry intervals */
+#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT (1<<2)
+
+/* mind the gap in values; see OAUTHDEBUG_UNSAFE_MASK below */
+
+/* print PQconnectPoll statistics */
+#define OAUTHDEBUG_CALL_COUNT (1<<16)
+/* print plugin loading errors */
+#define OAUTHDEBUG_PLUGIN_ERRORS (1<<17)
+
+/* all safe and unsafe flags, for the legacy UNSAFE behavior */
+#define OAUTHDEBUG_UNSAFE_ALL ((uint32) ~0)
+
+/* Flags are divided into "safe" and "unsafe" based on bit position. */
+#define OAUTHDEBUG_UNSAFE_MASK ((uint32) 0x0000FFFF)
+
+static_assert(OAUTHDEBUG_CALL_COUNT == OAUTHDEBUG_UNSAFE_MASK + 1,
+ "the first safe OAUTHDEBUG flag should be above OAUTHDEBUG_UNSAFE_MASK");
extern void pqClearOAuthToken(PGconn *conn);
-extern oauth_debug_flags oauth_get_debug_flags(void);
+extern uint32 oauth_get_debug_flags(void);
/* Mechanisms in fe-auth-oauth.c */
extern const pg_fe_sasl_mech pg_oauth_mech;
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 564d76cf063..7100824c560 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -274,7 +274,7 @@ struct async_ctx
int running; /* is asynchronous work in progress? */
bool user_prompted; /* have we already sent the authz prompt? */
bool used_basic_auth; /* did we send a client secret? */
- oauth_debug_flags debug_flags; /* can we give developer assistance */
+ uint32 debug_flags; /* can we give developer assistance? */
int dbg_num_calls; /* (debug mode) how many times were we called? */
};
@@ -1023,7 +1023,7 @@ parse_interval(struct async_ctx *actx, const char *interval_str)
parsed = ceil(parsed);
if (parsed < 1)
- return actx->debug_flags.fast_retry ? 0 : 1;
+ return (actx->debug_flags & OAUTHDEBUG_UNSAFE_DOS_ENDPOINT) ? 0 : 1;
else if (parsed >= INT_MAX)
return INT_MAX;
@@ -1797,7 +1797,7 @@ setup_curl_handles(struct async_ctx *actx)
*/
CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
- if (actx->debug_flags.trace)
+ if (actx->debug_flags & OAUTHDEBUG_UNSAFE_TRACE)
{
/*
* Set a callback for retrieving error information from libcurl, the
@@ -1829,7 +1829,7 @@ setup_curl_handles(struct async_ctx *actx)
const long unsafe = CURLPROTO_HTTPS | CURLPROTO_HTTP;
#endif
- if (actx->debug_flags.http)
+ if (actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP)
protos = unsafe;
CHECK_SETOPT(actx, popt, protos, return false);
@@ -2297,7 +2297,7 @@ check_for_device_flow(struct async_ctx *actx)
* decent time to bail out if we're not using HTTPS for the endpoints
* we'll use for the flow.
*/
- if (!actx->debug_flags.http)
+ if ((actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP) == 0)
{
if (pg_strncasecmp(provider->device_authorization_endpoint,
HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
@@ -3027,7 +3027,7 @@ pg_fe_run_oauth_flow(PGconn *conn, struct PGoauthBearerRequest *request,
* drain_timer_events(), when we're in debug mode, track the total number
* of calls to this function and print that at the end of the flow.
*/
- if (actx && actx->debug_flags.poll_counts)
+ if (actx->debug_flags & OAUTHDEBUG_CALL_COUNT)
{
actx->dbg_num_calls++;
if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
diff --git a/src/interfaces/libpq-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c
index 06815be9a0a..185c17e5807 100644
--- a/src/interfaces/libpq-oauth/test-oauth-curl.c
+++ b/src/interfaces/libpq-oauth/test-oauth-curl.c
@@ -89,11 +89,7 @@ init_test_actx(void)
actx->mux = PGINVALID_SOCKET;
actx->timerfd = -1;
- actx->debug_flags.http = true;
- actx->debug_flags.trace = true;
- actx->debug_flags.fast_retry = true;
- actx->debug_flags.poll_counts = true;
- actx->debug_flags.print_plugin_errors = true;
+ actx->debug_flags = OAUTHDEBUG_UNSAFE_ALL;
initPQExpBuffer(&actx->errbuf);
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
index 957da5d4068..c9a82b3f78e 100644
--- a/src/interfaces/libpq/fe-auth-oauth-debug.c
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -30,38 +30,38 @@
* Sets *is_unsafe to indicate if this option requires the UNSAFE: prefix.
*/
static bool
-parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe)
+parse_debug_option(const char *option, uint32 *flags, bool *is_unsafe)
{
*is_unsafe = false;
/* Unsafe options */
if (strcmp(option, "http") == 0)
{
- flags->http = true;
+ *flags |= OAUTHDEBUG_UNSAFE_HTTP;
*is_unsafe = true;
return true;
}
else if (strcmp(option, "trace") == 0)
{
- flags->trace = true;
+ *flags |= OAUTHDEBUG_UNSAFE_TRACE;
*is_unsafe = true;
return true;
}
else if (strcmp(option, "dos-endpoint") == 0)
{
- flags->fast_retry = true;
+ *flags |= OAUTHDEBUG_UNSAFE_DOS_ENDPOINT;
*is_unsafe = true;
return true;
}
/* Safe options */
else if (strcmp(option, "call-count") == 0)
{
- flags->poll_counts = true;
+ *flags |= OAUTHDEBUG_CALL_COUNT;
return true;
}
else if (strcmp(option, "plugin-errors") == 0)
{
- flags->print_plugin_errors = true;
+ *flags |= OAUTHDEBUG_PLUGIN_ERRORS;
return true;
}
@@ -80,10 +80,10 @@ parse_debug_option(const char *option, oauth_debug_flags *flags, bool *is_unsafe
* - An unrecognized option is specified
* - An unsafe option is specified without the UNSAFE: prefix
*/
-oauth_debug_flags
+uint32
oauth_get_debug_flags(void)
{
- oauth_debug_flags flags = {0};
+ uint32 flags = 0;
const char *env = getenv("PGOAUTHDEBUG");
char *options_str;
char *option;
@@ -94,14 +94,7 @@ oauth_get_debug_flags(void)
return flags;
if (strcmp(env, "UNSAFE") == 0)
- {
- flags.http = true;
- flags.trace = true;
- flags.fast_retry = true;
- flags.poll_counts = true;
- flags.print_plugin_errors = true;
- return flags;
- }
+ return OAUTHDEBUG_UNSAFE_ALL;
if (strncmp(env, "UNSAFE:", 7) == 0)
{
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 5f5900a9ae7..6f7ec3a129e 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -389,7 +389,7 @@ issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
authority_start = wkuri + strlen(HTTPS_SCHEME);
if (!authority_start
- && oauth_get_debug_flags().http
+ && (oauth_get_debug_flags() & OAUTHDEBUG_UNSAFE_HTTP)
&& pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
{
/* Allow http:// for testing only. */
@@ -900,7 +900,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
*
* Note that POSIX dlerror() isn't guaranteed to be threadsafe.
*/
- if (oauth_get_debug_flags().print_plugin_errors)
+ if (oauth_get_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
return 0;
@@ -922,7 +922,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
* cause is still locked behind PGOAUTHDEBUG due to the dlerror()
* threadsafety issue.
*/
- if (oauth_get_debug_flags().print_plugin_errors)
+ if (oauth_get_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->flow_module);
--
2.34.1
[application/octet-stream] v3.1-0004-squash-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-.patch (4.5K, 5-v3.1-0004-squash-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-.patch)
download | inline diff:
From 59aca1b12a8b6c71edc2f15e910f0950a012aa63 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Wed, 1 Apr 2026 11:05:46 -0700
Subject: [PATCH v3.1 4/6] squash! Split PGOAUTHDEBUG=UNSAFE into multiple
options
- Set specific debug flags in 001_server.pl.
- Add failing test to ensure ignored flags are actually ignored. Fix by
inlining parse_debug_option().
---
src/interfaces/libpq/fe-auth-oauth-debug.c | 66 +++++--------------
.../modules/oauth_validator/t/001_server.pl | 22 ++++++-
2 files changed, 38 insertions(+), 50 deletions(-)
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/fe-auth-oauth-debug.c
index c9a82b3f78e..8bf710ce46a 100644
--- a/src/interfaces/libpq/fe-auth-oauth-debug.c
+++ b/src/interfaces/libpq/fe-auth-oauth-debug.c
@@ -24,50 +24,6 @@
#include "fe-auth-oauth.h"
-/*
- * Parse a single debug option from PGOAUTHDEBUG.
- * Returns true if the option is recognized, false otherwise.
- * Sets *is_unsafe to indicate if this option requires the UNSAFE: prefix.
- */
-static bool
-parse_debug_option(const char *option, uint32 *flags, bool *is_unsafe)
-{
- *is_unsafe = false;
-
- /* Unsafe options */
- if (strcmp(option, "http") == 0)
- {
- *flags |= OAUTHDEBUG_UNSAFE_HTTP;
- *is_unsafe = true;
- return true;
- }
- else if (strcmp(option, "trace") == 0)
- {
- *flags |= OAUTHDEBUG_UNSAFE_TRACE;
- *is_unsafe = true;
- return true;
- }
- else if (strcmp(option, "dos-endpoint") == 0)
- {
- *flags |= OAUTHDEBUG_UNSAFE_DOS_ENDPOINT;
- *is_unsafe = true;
- return true;
- }
- /* Safe options */
- else if (strcmp(option, "call-count") == 0)
- {
- *flags |= OAUTHDEBUG_CALL_COUNT;
- return true;
- }
- else if (strcmp(option, "plugin-errors") == 0)
- {
- *flags |= OAUTHDEBUG_PLUGIN_ERRORS;
- return true;
- }
-
- return false;
-}
-
/*
* Parses the PGOAUTHDEBUG environment variable and returns debug flags.
*
@@ -109,22 +65,36 @@ oauth_get_debug_flags(void)
option = strtok_r(options_str, ",", &saveptr);
while (option != NULL)
{
- bool is_unsafe;
-
- if (!parse_debug_option(option, &flags, &is_unsafe))
+ uint32 flag = 0;
+
+ if (strcmp(option, "http") == 0)
+ flag = OAUTHDEBUG_UNSAFE_HTTP;
+ else if (strcmp(option, "trace") == 0)
+ flag = OAUTHDEBUG_UNSAFE_TRACE;
+ else if (strcmp(option, "dos-endpoint") == 0)
+ flag = OAUTHDEBUG_UNSAFE_DOS_ENDPOINT;
+ else if (strcmp(option, "call-count") == 0)
+ flag = OAUTHDEBUG_CALL_COUNT;
+ else if (strcmp(option, "plugin-errors") == 0)
+ flag = OAUTHDEBUG_PLUGIN_ERRORS;
+ else
{
fprintf(stderr,
"WARNING: PGOAUTHDEBUG: unrecognized debug option \"%s\" (ignored)\n",
option);
}
- else if (is_unsafe && !unsafe_prefix)
+
+ if (!unsafe_prefix && ((flag & OAUTHDEBUG_UNSAFE_MASK) != 0))
{
+ flag = 0;
+
fprintf(stderr,
"WARNING: PGOAUTHDEBUG: unsafe option \"%s\" requires UNSAFE: prefix (ignored)\n"
"Use: PGOAUTHDEBUG=UNSAFE:%s\n",
option, option);
}
+ flags |= flag;
option = strtok_r(NULL, ",", &saveptr);
}
diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index c9c46e63539..3803dd08287 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -93,6 +93,21 @@ $node->connect_fails(
qr@OAuth discovery URI "\Q$issuer\E/.well-known/openid-configuration" must use HTTPS@
);
+{
+ # PGOAUTHDEBUG=http should have no effect (it needs an UNSAFE: marker).
+ local $ENV{PGOAUTHDEBUG} = "http";
+
+ $node->connect_fails(
+ "user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
+ "HTTPS is required without debug mode (bad PGOAUTHDEBUG value)",
+ expected_stderr => qr[
+ ^WARNING: .* \Qunsafe option "http" requires UNSAFE: prefix\E
+ .*
+ \QOAuth discovery URI "$issuer/.well-known/openid-configuration" must use HTTPS\E
+ ]msx
+ );
+}
+
# Switch to HTTPS.
$issuer = "https://127.0.0.1:$port";
@@ -172,8 +187,11 @@ $node->connect_ok(
],
log_unlike => [qr/FATAL.*OAuth bearer authentication failed/]);
-# Enable PGOAUTHDEBUG for all remaining tests.
-$ENV{PGOAUTHDEBUG} = "UNSAFE";
+# Enable some debugging features for all remaining tests:
+# - trace, for detailed Curl logs on failure
+# - dos-endpoint, to speed up the three-way handshake
+# - call-count, for our later sanity check
+$ENV{PGOAUTHDEBUG} = "UNSAFE:trace,dos-endpoint,call-count";
# The /alternate issuer uses slightly different parameters, along with an
# OAuth-style discovery document.
--
2.34.1
[application/octet-stream] v3.1-0005-squash-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-.patch (9.2K, 6-v3.1-0005-squash-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-.patch)
download | inline diff:
From ec1318b9d169418c195f6e1625820921fde099a7 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Wed, 1 Apr 2026 11:17:36 -0700
Subject: [PATCH v3.1 5/6] squash! Split PGOAUTHDEBUG=UNSAFE into multiple
options
- Move the implementation into its own header; revert the build system
changes that were needed for the .c approach.
- <stdio.h> et al are included in "c.h" and do not need to be explicitly
pulled in.
---
src/interfaces/libpq-oauth/meson.build | 6 +--
src/interfaces/libpq/meson.build | 1 -
src/interfaces/libpq-oauth/Makefile | 11 +----
src/interfaces/libpq/Makefile | 3 +-
src/interfaces/libpq-oauth/oauth-utils.h | 2 -
src/interfaces/libpq/fe-auth-oauth.h | 30 ------------
.../{fe-auth-oauth-debug.c => oauth-debug.h} | 49 ++++++++++++++-----
src/interfaces/libpq-oauth/oauth-curl.c | 1 +
src/interfaces/libpq/fe-auth-oauth.c | 1 +
9 files changed, 43 insertions(+), 61 deletions(-)
rename src/interfaces/libpq/{fe-auth-oauth-debug.c => oauth-debug.h} (62%)
diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build
index d8cc92e0c2c..ea3a900f4f1 100644
--- a/src/interfaces/libpq-oauth/meson.build
+++ b/src/interfaces/libpq-oauth/meson.build
@@ -6,7 +6,6 @@ endif
libpq_oauth_sources = files(
'oauth-curl.c',
- '../libpq/fe-auth-oauth-debug.c',
)
# The shared library needs additional glue symbols.
@@ -63,10 +62,7 @@ endif
libpq_oauth_test_deps = []
-oauth_test_sources = files(
- 'test-oauth-curl.c',
- '../libpq/fe-auth-oauth-debug.c',
-) + libpq_oauth_so_sources
+oauth_test_sources = files('test-oauth-curl.c') + libpq_oauth_so_sources
if host_system == 'windows'
oauth_test_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index d031f4962e5..b0ae72167a1 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -2,7 +2,6 @@
libpq_sources = files(
'fe-auth-oauth.c',
- 'fe-auth-oauth-debug.c',
'fe-auth-scram.c',
'fe-auth.c',
'fe-cancel.c',
diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile
index c6097dda531..11e1a3cf528 100644
--- a/src/interfaces/libpq-oauth/Makefile
+++ b/src/interfaces/libpq-oauth/Makefile
@@ -36,24 +36,15 @@ override CPPFLAGS_SHLIB += -DUSE_PRIVATE_ENCODING_FUNCS
OBJS = \
$(WIN32RES)
-OBJS_STATIC = \
- oauth-curl.o \
- fe-auth-oauth-debug.o
+OBJS_STATIC = oauth-curl.o
# The shared library needs additional glue symbols.
OBJS_SHLIB = \
oauth-curl_shlib.o \
oauth-utils.o \
- fe-auth-oauth-debug_shlib.o
oauth-utils.o: override CPPFLAGS += $(CPPFLAGS_SHLIB)
-fe-auth-oauth-debug.o: $(libpq_srcdir)/fe-auth-oauth-debug.c
- $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
-
-fe-auth-oauth-debug_shlib.o: $(libpq_srcdir)/fe-auth-oauth-debug.c fe-auth-oauth-debug.o
- $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@
-
# Add shlib-/stlib-specific objects.
$(shlib): override OBJS += $(OBJS_SHLIB)
$(shlib): $(OBJS_SHLIB)
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 099c6557e77..0963995eed4 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -44,8 +44,7 @@ OBJS = \
legacy-pqsignal.o \
libpq-events.o \
pqexpbuffer.o \
- fe-auth.o \
- fe-auth-oauth-debug.o
+ fe-auth.o
# File shared across all SSL implementations supported.
ifneq ($(with_ssl),no)
diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h
index 64a9235ee85..dacd2dbacfe 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.h
+++ b/src/interfaces/libpq-oauth/oauth-utils.h
@@ -15,7 +15,6 @@
#ifndef OAUTH_UTILS_H
#define OAUTH_UTILS_H
-#include "fe-auth-oauth.h"
#include "libpq-fe.h"
#include "pqexpbuffer.h"
@@ -36,7 +35,6 @@ typedef enum
PG_BOOL_NO /* No (false) */
} PGTernaryBool;
-extern uint32 oauth_get_debug_flags(void);
extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe);
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index a952fea09cf..a50d7b03408 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -39,37 +39,7 @@ typedef struct
void *flow_module;
} fe_oauth_state;
-/*
- * Debug flags for the PGOAUTHDEBUG environment variable. Each flag controls a
- * specific debug feature. OAUTHDEBUG_UNSAFE_* flags require the envvar to have
- * a literal "UNSAFE:" prefix.
- */
-
-/* allow HTTP (unencrypted) connections */
-#define OAUTHDEBUG_UNSAFE_HTTP (1<<0)
-/* log HTTP traffic (exposes secrets) */
-#define OAUTHDEBUG_UNSAFE_TRACE (1<<1)
-/* allow zero-second retry intervals */
-#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT (1<<2)
-
-/* mind the gap in values; see OAUTHDEBUG_UNSAFE_MASK below */
-
-/* print PQconnectPoll statistics */
-#define OAUTHDEBUG_CALL_COUNT (1<<16)
-/* print plugin loading errors */
-#define OAUTHDEBUG_PLUGIN_ERRORS (1<<17)
-
-/* all safe and unsafe flags, for the legacy UNSAFE behavior */
-#define OAUTHDEBUG_UNSAFE_ALL ((uint32) ~0)
-
-/* Flags are divided into "safe" and "unsafe" based on bit position. */
-#define OAUTHDEBUG_UNSAFE_MASK ((uint32) 0x0000FFFF)
-
-static_assert(OAUTHDEBUG_CALL_COUNT == OAUTHDEBUG_UNSAFE_MASK + 1,
- "the first safe OAUTHDEBUG flag should be above OAUTHDEBUG_UNSAFE_MASK");
-
extern void pqClearOAuthToken(PGconn *conn);
-extern uint32 oauth_get_debug_flags(void);
/* Mechanisms in fe-auth-oauth.c */
extern const pg_fe_sasl_mech pg_oauth_mech;
diff --git a/src/interfaces/libpq/fe-auth-oauth-debug.c b/src/interfaces/libpq/oauth-debug.h
similarity index 62%
rename from src/interfaces/libpq/fe-auth-oauth-debug.c
rename to src/interfaces/libpq/oauth-debug.h
index 8bf710ce46a..ad5246a8402 100644
--- a/src/interfaces/libpq/fe-auth-oauth-debug.c
+++ b/src/interfaces/libpq/oauth-debug.h
@@ -1,28 +1,53 @@
/*-------------------------------------------------------------------------
*
- * fe-auth-oauth-debug.c
+ * oauth-debug.h
* Parsing logic for PGOAUTHDEBUG environment variable
*
- * This file contains pure string parsing logic with no dependencies on
- * libpq or libpq-oauth implementation details. It's compiled into both
- * libraries to avoid code duplication.
+ * Both libpq and libpq-oauth need this logic, so it's packaged in a small
+ * header for convenience.
*
- * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
* IDENTIFICATION
- * src/interfaces/libpq/fe-auth-oauth-debug.c
+ * src/interfaces/libpq/oauth-debug.h
*
*-------------------------------------------------------------------------
*/
+#ifndef OAUTH_DEBUG_H
+#define OAUTH_DEBUG_H
+
#include "postgres_fe.h"
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
+/*
+ * Debug flags for the PGOAUTHDEBUG environment variable. Each flag controls a
+ * specific debug feature. OAUTHDEBUG_UNSAFE_* flags require the envvar to have
+ * a literal "UNSAFE:" prefix.
+ */
+
+/* allow HTTP (unencrypted) connections */
+#define OAUTHDEBUG_UNSAFE_HTTP (1<<0)
+/* log HTTP traffic (exposes secrets) */
+#define OAUTHDEBUG_UNSAFE_TRACE (1<<1)
+/* allow zero-second retry intervals */
+#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT (1<<2)
+
+/* mind the gap in values; see OAUTHDEBUG_UNSAFE_MASK below */
-#include "fe-auth-oauth.h"
+/* print PQconnectPoll statistics */
+#define OAUTHDEBUG_CALL_COUNT (1<<16)
+/* print plugin loading errors */
+#define OAUTHDEBUG_PLUGIN_ERRORS (1<<17)
+
+/* all safe and unsafe flags, for the legacy UNSAFE behavior */
+#define OAUTHDEBUG_UNSAFE_ALL ((uint32) ~0)
+
+/* Flags are divided into "safe" and "unsafe" based on bit position. */
+#define OAUTHDEBUG_UNSAFE_MASK ((uint32) 0x0000FFFF)
+
+static_assert(OAUTHDEBUG_CALL_COUNT == OAUTHDEBUG_UNSAFE_MASK + 1,
+ "the first safe OAUTHDEBUG flag should be above OAUTHDEBUG_UNSAFE_MASK");
/*
* Parses the PGOAUTHDEBUG environment variable and returns debug flags.
@@ -36,7 +61,7 @@
* - An unrecognized option is specified
* - An unsafe option is specified without the UNSAFE: prefix
*/
-uint32
+static uint32
oauth_get_debug_flags(void)
{
uint32 flags = 0;
@@ -102,3 +127,5 @@ oauth_get_debug_flags(void)
return flags;
}
+
+#endif /* OAUTH_DEBUG_H */
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 7100824c560..5ee630dd9f7 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -31,6 +31,7 @@
#include "common/jsonapi.h"
#include "mb/pg_wchar.h"
#include "oauth-curl.h"
+#include "oauth-debug.h"
#ifdef USE_DYNAMIC_OAUTH
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 6f7ec3a129e..c150f27df00 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -26,6 +26,7 @@
#include "fe-auth.h"
#include "fe-auth-oauth.h"
#include "mb/pg_wchar.h"
+#include "oauth-debug.h"
#include "pg_config_paths.h"
#include "utils/memdebug.h"
--
2.34.1
[application/octet-stream] v3.1-0006-squash-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-.patch (5.2K, 7-v3.1-0006-squash-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-.patch)
download | inline diff:
From a79b31835ce3e9eb46f9fd7505d83e9a00b0b6e4 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Wed, 1 Apr 2026 11:22:15 -0700
Subject: [PATCH v3.1 6/6] squash! Split PGOAUTHDEBUG=UNSAFE into multiple
options
- Translate the warnings, since they're a safety feature for confused
end users. Requires a small complication for libpq_gettext().
- Update warning text. Do not provide instructions on how to defeat the
UNSAFE protection; if a user doesn't know how to do that immediately,
they should absolutely not be using an unsafe dev option.
- nit: rename unsafe_prefix to unsafe_allowed
---
src/interfaces/libpq/oauth-debug.h | 25 +++++++++++--------
src/interfaces/libpq-oauth/oauth-curl.c | 7 +++++-
.../modules/oauth_validator/t/001_server.pl | 2 +-
src/tools/pginclude/headerscheck | 2 ++
4 files changed, 24 insertions(+), 12 deletions(-)
diff --git a/src/interfaces/libpq/oauth-debug.h b/src/interfaces/libpq/oauth-debug.h
index ad5246a8402..0bd8467a09c 100644
--- a/src/interfaces/libpq/oauth-debug.h
+++ b/src/interfaces/libpq/oauth-debug.h
@@ -4,7 +4,8 @@
* Parsing logic for PGOAUTHDEBUG environment variable
*
* Both libpq and libpq-oauth need this logic, so it's packaged in a small
- * header for convenience.
+ * header for convenience. This is not quite a standalone header, due to the
+ * complication introduced by libpq_gettext(); see note below.
*
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
@@ -20,6 +21,13 @@
#include "postgres_fe.h"
+/*
+ * XXX libpq-oauth can't compile against libpq-int.h, so clients of this header
+ * need to provide the declaration of libpq_gettext() before #including it.
+ * Fortunately, there are only two such clients.
+ */
+/* #include "libpq-int.h" */
+
/*
* Debug flags for the PGOAUTHDEBUG environment variable. Each flag controls a
* specific debug feature. OAUTHDEBUG_UNSAFE_* flags require the envvar to have
@@ -69,7 +77,7 @@ oauth_get_debug_flags(void)
char *options_str;
char *option;
char *saveptr = NULL;
- bool unsafe_prefix = false;
+ bool unsafe_allowed = false;
if (!env || env[0] == '\0')
return flags;
@@ -79,7 +87,7 @@ oauth_get_debug_flags(void)
if (strncmp(env, "UNSAFE:", 7) == 0)
{
- unsafe_prefix = true;
+ unsafe_allowed = true;
env += 7;
}
@@ -103,20 +111,17 @@ oauth_get_debug_flags(void)
else if (strcmp(option, "plugin-errors") == 0)
flag = OAUTHDEBUG_PLUGIN_ERRORS;
else
- {
fprintf(stderr,
- "WARNING: PGOAUTHDEBUG: unrecognized debug option \"%s\" (ignored)\n",
+ libpq_gettext("WARNING: unrecognized PGOAUTHDEBUG option \"%s\" (ignored)\n"),
option);
- }
- if (!unsafe_prefix && ((flag & OAUTHDEBUG_UNSAFE_MASK) != 0))
+ if (!unsafe_allowed && ((flag & OAUTHDEBUG_UNSAFE_MASK) != 0))
{
flag = 0;
fprintf(stderr,
- "WARNING: PGOAUTHDEBUG: unsafe option \"%s\" requires UNSAFE: prefix (ignored)\n"
- "Use: PGOAUTHDEBUG=UNSAFE:%s\n",
- option, option);
+ libpq_gettext("WARNING: PGOAUTHDEBUG option \"%s\" is unsafe (ignored)\n"),
+ option);
}
flags |= flag;
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 5ee630dd9f7..eb2fe35d0cc 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -31,7 +31,6 @@
#include "common/jsonapi.h"
#include "mb/pg_wchar.h"
#include "oauth-curl.h"
-#include "oauth-debug.h"
#ifdef USE_DYNAMIC_OAUTH
@@ -50,6 +49,12 @@
#endif /* USE_DYNAMIC_OAUTH */
+/*
+ * oauth-debug.h needs the declaration of libpq_gettext(), from one of the above
+ * sources.
+ */
+#include "oauth-debug.h"
+
/* One final guardrail against accidental inclusion... */
#if defined(USE_DYNAMIC_OAUTH) && defined(LIBPQ_INT_H)
#error do not rely on libpq-int.h in dynamic builds of libpq-oauth
diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index 3803dd08287..3d190c2ba71 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -101,7 +101,7 @@ $node->connect_fails(
"user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
"HTTPS is required without debug mode (bad PGOAUTHDEBUG value)",
expected_stderr => qr[
- ^WARNING: .* \Qunsafe option "http" requires UNSAFE: prefix\E
+ ^WARNING: .* \Qoption "http" is unsafe\E
.*
\QOAuth discovery URI "$issuer/.well-known/openid-configuration" must use HTTPS\E
]msx
diff --git a/src/tools/pginclude/headerscheck b/src/tools/pginclude/headerscheck
index 14c466cc237..de50b6937af 100755
--- a/src/tools/pginclude/headerscheck
+++ b/src/tools/pginclude/headerscheck
@@ -153,6 +153,8 @@ do
test "$f" = src/include/catalog/syscache_ids.h && continue
test "$f" = src/include/catalog/syscache_info.h && continue
+ test "$f" = src/interfaces/libpq/oauth-debug.h && continue
+
# We can't make these Bison output files compilable standalone
# without using "%code require", which old Bison versions lack.
# parser/gram.h will be included by parser/gramparse.h anyway.
--
2.34.1
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-04-01 21:12 Zsolt Parragi <[email protected]>
parent: Jacob Champion <[email protected]>
0 siblings, 2 replies; 13+ messages in thread
From: Zsolt Parragi @ 2026-04-01 21:12 UTC (permalink / raw)
To: Jacob Champion <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
> OAUTHDEBUG_LEGACY_UNSAFE?
That sounds better
> I think I'm missing something; how does the choice of .c/.h change
> things? There's no static tracking in v1 of the patchset
Eh, sorry about that, I was sure that I sent a version which handled
that to the list, but apparently I didn't. It didn't use
atomics/mutexes, so maybe it's better.
> `UNSAFE` is intended to be a weak defense against social engineering
> attacks. So these warnings need to be translated, if possible, and we
> should not provide instructions on how to defeat that defense.
With the same logic, shouldn't we print a very visible warning when
somebody enables trace? Since it's a long output, maybe to both the
beginning and end of the flow?
Attachments:
[application/octet-stream] nocfbot-tracewarning.diff (1.1K, 2-nocfbot-tracewarning.diff)
download | inline diff:
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index eb2fe35d0cc..ad8c8c4565b 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -3041,6 +3041,11 @@ pg_fe_run_oauth_flow(PGconn *conn, struct PGoauthBearerRequest *request,
actx->dbg_num_calls);
}
+ if ((actx->debug_flags & OAUTHDEBUG_UNSAFE_TRACE)
+ && (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED))
+ fprintf(stderr,
+ libpq_gettext("WARNING: PGOAUTHDEBUG trace output above may contain secrets. Do not share with third parties.\n"));
+
#ifndef WIN32
if (masked)
{
@@ -3096,6 +3101,10 @@ pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request)
/* Parse debug flags from the environment. */
actx->debug_flags = oauth_get_debug_flags();
+ if (actx->debug_flags & OAUTHDEBUG_UNSAFE_TRACE)
+ fprintf(stderr,
+ libpq_gettext("WARNING: PGOAUTHDEBUG trace is enabled. HTTP traffic (including secrets) will be logged.\n"));
+
initPQExpBuffer(&actx->work_data);
initPQExpBuffer(&actx->errbuf);
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-04-01 22:59 Jacob Champion <[email protected]>
parent: Zsolt Parragi <[email protected]>
1 sibling, 0 replies; 13+ messages in thread
From: Jacob Champion @ 2026-04-01 22:59 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Wed, Apr 1, 2026 at 2:13 PM Zsolt Parragi <[email protected]> wrote:
> With the same logic, shouldn't we print a very visible warning when
> somebody enables trace? Since it's a long output, maybe to both the
> beginning and end of the flow?
I'm more than happy to strengthen this as well, but let's kick that
out to its own thread, especially if pieces are backpatchable.
--Jacob
^ permalink raw reply [nested|flat] 13+ messages in thread
* Re: [oauth] Split and extend PGOAUTHDEBUG
@ 2026-04-03 17:20 Jacob Champion <[email protected]>
parent: Zsolt Parragi <[email protected]>
1 sibling, 0 replies; 13+ messages in thread
From: Jacob Champion @ 2026-04-03 17:20 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Wed, Apr 1, 2026 at 2:13 PM Zsolt Parragi <[email protected]> wrote:
>
> > OAUTHDEBUG_LEGACY_UNSAFE?
>
> That sounds better
Done. v4 also renames oauth_get_debug_flags() to
oauth_parse_debug_flags() (and adds a related doc comment) after Chao
Li's feedback, and tightens up some of the new documentation.
Thanks,
--Jacob
Attachments:
[application/octet-stream] since-v3.nocfbot.diff (6.9K, 2-since-v3.nocfbot.diff)
download | inline diff:
1: 743b4a5c3a5 ! 1: c65877b0b59 Split PGOAUTHDEBUG=UNSAFE into multiple options
@@ Metadata
## Commit message ##
Split PGOAUTHDEBUG=UNSAFE into multiple options
- WIP
+ PGOAUTHDEBUG is a blunt instrument: you get all the debugging features,
+ or nothing. The most annoying consequence during manual use is the Curl
+ debug trace, which tends to obscure the device flow prompt entirely. The
+ promotion of PGOAUTHCAFILE into its own feature in 993368113 improves
+ the situation somewhat, but there's still the discomfort of knowing you
+ have to opt into many dangerous behaviors just to get the single debug
+ feature you wanted.
+
+ Explode the PGOAUTHDEBUG syntax into a comma-separated list. The old
+ "UNSAFE" value enables everything, like before. Any individual unsafe
+ features still require the envvar to begin with an "UNSAFE:" prefix, to
+ try to interrupt the flow of someone who is about to do something they
+ should not.
+
+ So now, rather than
+
+ PGOAUTHDEBUG=UNSAFE # enable all the unsafe things
+
+ a developer can say
+
+ PGOAUTHDEBUG=call-count # only show me the call count. safe!
+ PGOAUTHDEBUG=UNSAFE:trace # print secrets, but don't allow HTTP
+
+ To avoid adding more build system scaffolding to libpq-oauth, implement
+ this entirely in a small private header. This unfortunately can't be
+ standalone, so it needs a headerscheck exception.
Author: Zsolt Parragi <[email protected]>
Co-authored-by: Jacob Champion <[email protected]>
+ Reviewed-by: Chao Li <[email protected]>
+ Reviewed-by: Zsolt Parragi <[email protected]>
+ Discussion: https://postgr.es/m/CAOYmi%2B%3DfbZNJSkHVci%3DGpR8XPYObK%3DH%2B2ERRha0LDTS%2BifsWnw%40mail.gmail.com
+ Discussion: https://postgr.es/m/CAN4CZFMmDZMH56O9vb_g7vHqAk8ryWFxBMV19C39PFghENg8kA%40mail.gmail.com
## doc/src/sgml/libpq.sgml ##
@@ doc/src/sgml/libpq.sgml: typedef struct
@@ doc/src/sgml/libpq.sgml: typedef struct
+ </listitem>
+ </varlistentry>
+ </variablelist>
-+ </para>
+ </para>
+
+ <para>
+ Unsafe options (<literal>http</literal>, <literal>trace</literal>,
+ <literal>dos-endpoint</literal>) require the <literal>UNSAFE:</literal> prefix.
-+ If unsafe options are specified without this prefix, a warning is printed
-+ to standard error and that option is ignored. Other valid options in the
-+ list continue to work. Safe options (<literal>call-count</literal>,
-+ <literal>plugin-errors</literal>) can be used without the prefix.
- </para>
-+
-+ <para>
-+ Unrecognized option names will also trigger a warning and be ignored, while
-+ valid options continue to work. This helps catch typos in the environment
-+ variable configuration without breaking the debugging of valid options.
++ 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
++ (<literal>call-count</literal>, <literal>plugin-errors</literal>) can be
++ used without the prefix.
+ </para>
+
+ <para>
@@ src/interfaces/libpq/oauth-debug.h (new)
+#define OAUTHDEBUG_PLUGIN_ERRORS (1<<17)
+
+/* all safe and unsafe flags, for the legacy UNSAFE behavior */
-+#define OAUTHDEBUG_UNSAFE_ALL ((uint32) ~0)
++#define OAUTHDEBUG_LEGACY_UNSAFE ((uint32) ~0)
+
+/* Flags are divided into "safe" and "unsafe" based on bit position. */
+#define OAUTHDEBUG_UNSAFE_MASK ((uint32) 0x0000FFFF)
@@ src/interfaces/libpq/oauth-debug.h (new)
+ * Prints a warning and skips the invalid option if:
+ * - An unrecognized option is specified
+ * - An unsafe option is specified without the UNSAFE: prefix
++ *
++ * XXX The parsing, and any warnings, will happen each time the function is
++ * called, so consider caching the result in cases where that might get
++ * annoying. But don't try to cache inside this function, unless you also have a
++ * plan for getting libpq and libpq-oauth to share that cache safely... probably
++ * not worth the effort for a debugging aid?
+ */
+static uint32
-+oauth_get_debug_flags(void)
++oauth_parse_debug_flags(void)
+{
+ uint32 flags = 0;
+ const char *env = getenv("PGOAUTHDEBUG");
@@ src/interfaces/libpq/oauth-debug.h (new)
+ return flags;
+
+ if (strcmp(env, "UNSAFE") == 0)
-+ return OAUTHDEBUG_UNSAFE_ALL;
++ return OAUTHDEBUG_LEGACY_UNSAFE;
+
+ if (strncmp(env, "UNSAFE:", 7) == 0)
+ {
@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_start_oauthbearer(PGconn *conn, PGoa
- /* Should we enable unsafe features? */
- actx->debugging = oauth_unsafe_debugging_enabled();
+ /* Parse debug flags from the environment. */
-+ actx->debug_flags = oauth_get_debug_flags();
++ actx->debug_flags = oauth_parse_debug_flags();
initPQExpBuffer(&actx->work_data);
initPQExpBuffer(&actx->errbuf);
@@ src/interfaces/libpq-oauth/test-oauth-curl.c: init_test_actx(void)
actx->mux = PGINVALID_SOCKET;
actx->timerfd = -1;
- actx->debugging = true;
-+ actx->debug_flags = OAUTHDEBUG_UNSAFE_ALL;
++ actx->debug_flags = OAUTHDEBUG_LEGACY_UNSAFE;
initPQExpBuffer(&actx->errbuf);
@@ src/interfaces/libpq/fe-auth-oauth.c: issuer_from_well_known_uri(PGconn *conn, c
if (!authority_start
- && oauth_unsafe_debugging_enabled()
-+ && (oauth_get_debug_flags() & OAUTHDEBUG_UNSAFE_HTTP)
++ && (oauth_parse_debug_flags() & OAUTHDEBUG_UNSAFE_HTTP)
&& pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
{
/* Allow http:// for testing only. */
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
* Note that POSIX dlerror() isn't guaranteed to be threadsafe.
*/
- if (oauth_unsafe_debugging_enabled())
-+ if (oauth_get_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
++ if (oauth_parse_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
return 0;
@@ src/interfaces/libpq/fe-auth-oauth.c: use_builtin_flow(PGconn *conn, fe_oauth_st
* threadsafety issue.
*/
- if (oauth_unsafe_debugging_enabled())
-+ if (oauth_get_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
++ if (oauth_parse_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->flow_module);
[application/octet-stream] v4-0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch (21.0K, 3-v4-0001-Split-PGOAUTHDEBUG-UNSAFE-into-multiple-options.patch)
download | inline diff:
From c65877b0b59746f7291cc9568ae7167d10bb086f Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email protected]>
Date: Tue, 31 Mar 2026 14:08:59 -0700
Subject: [PATCH v4] Split PGOAUTHDEBUG=UNSAFE into multiple options
PGOAUTHDEBUG is a blunt instrument: you get all the debugging features,
or nothing. The most annoying consequence during manual use is the Curl
debug trace, which tends to obscure the device flow prompt entirely. The
promotion of PGOAUTHCAFILE into its own feature in 993368113 improves
the situation somewhat, but there's still the discomfort of knowing you
have to opt into many dangerous behaviors just to get the single debug
feature you wanted.
Explode the PGOAUTHDEBUG syntax into a comma-separated list. The old
"UNSAFE" value enables everything, like before. Any individual unsafe
features still require the envvar to begin with an "UNSAFE:" prefix, to
try to interrupt the flow of someone who is about to do something they
should not.
So now, rather than
PGOAUTHDEBUG=UNSAFE # enable all the unsafe things
a developer can say
PGOAUTHDEBUG=call-count # only show me the call count. safe!
PGOAUTHDEBUG=UNSAFE:trace # print secrets, but don't allow HTTP
To avoid adding more build system scaffolding to libpq-oauth, implement
this entirely in a small private header. This unfortunately can't be
standalone, so it needs a headerscheck exception.
Author: Zsolt Parragi <[email protected]>
Co-authored-by: Jacob Champion <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Discussion: https://postgr.es/m/CAOYmi%2B%3DfbZNJSkHVci%3DGpR8XPYObK%3DH%2B2ERRha0LDTS%2BifsWnw%40mail.gmail.com
Discussion: https://postgr.es/m/CAN4CZFMmDZMH56O9vb_g7vHqAk8ryWFxBMV19C39PFghENg8kA%40mail.gmail.com
---
doc/src/sgml/libpq.sgml | 119 ++++++++++++---
src/interfaces/libpq-oauth/oauth-utils.h | 1 -
src/interfaces/libpq/fe-auth-oauth.h | 1 -
src/interfaces/libpq/oauth-debug.h | 142 ++++++++++++++++++
src/interfaces/libpq-oauth/oauth-curl.c | 22 ++-
src/interfaces/libpq-oauth/oauth-utils.c | 11 --
src/interfaces/libpq-oauth/test-oauth-curl.c | 2 +-
src/interfaces/libpq/fe-auth-oauth.c | 18 +--
.../modules/oauth_validator/t/001_server.pl | 22 ++-
src/tools/pginclude/headerscheck | 2 +
10 files changed, 277 insertions(+), 63 deletions(-)
create mode 100644 src/interfaces/libpq/oauth-debug.h
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index a48d3161495..0a19c2b553b 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -10643,35 +10643,104 @@ typedef struct
</para>
<para>
- A "dangerous debugging mode" may be enabled by setting the environment
- variable <envar>PGOAUTHDEBUG=UNSAFE</envar>. This functionality is provided
- for ease of local development and testing only. It does several things that
- you will not want a production system to do:
+ Debug features may be enabled by setting the <envar>PGOAUTHDEBUG</envar>
+ environment variable. This functionality is provided for ease of local
+ development and testing. The variable accepts a comma-separated list of
+ debug options:
+
+ <programlisting>
+PGOAUTHDEBUG=option1,option2,... <lineannotation>for safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:option1,option2,... <lineannotation>when using unsafe options</lineannotation>
+PGOAUTHDEBUG=UNSAFE <lineannotation>legacy format; enables all options</lineannotation>
+ </programlisting>
+ </para>
- <itemizedlist spacing="compact">
- <listitem>
- <para>
- permits the use of unencrypted HTTP during the OAuth provider exchange
- </para>
- </listitem>
- <listitem>
- <para>
- prints HTTP traffic (containing several critical secrets) to standard
- error during the OAuth flow
- </para>
- </listitem>
- <listitem>
- <para>
- permits the use of zero-second retry intervals, which can cause the
- client to busy-loop and pointlessly consume CPU
- </para>
- </listitem>
- </itemizedlist>
+ <para>
+ Available debug options:
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>http</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Permits the use of unencrypted HTTP during the OAuth provider exchange.
+ This allows OAuth credentials to be transmitted over unencrypted
+ connections, which is extremely dangerous and should only be used for
+ local testing.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>trace</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Prints HTTP traffic to standard error during the OAuth flow. This output
+ contains critical secrets including bearer tokens, client secrets, access
+ tokens, and authorization codes. Never share this output with third
+ parties.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>dos-endpoint</literal> (unsafe)</term>
+ <listitem>
+ <para>
+ Permits the use of zero-second retry intervals instead of the normal
+ minimum of one second. This speeds up tests, but in normal operation it
+ will cause the client to busy-loop, consuming CPU and network resources.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>call-count</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints the total number of calls to the flow plugin to standard error
+ when the OAuth flow completes. This helps developers debug the async
+ callback behavior.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>plugin-errors</literal> (safe)</term>
+ <listitem>
+ <para>
+ Prints plugin loading errors to standard error. This helps developers
+ and package maintainers debug issues when the OAuth plugin fails to load.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
</para>
+
+ <para>
+ Unsafe options (<literal>http</literal>, <literal>trace</literal>,
+ <literal>dos-endpoint</literal>) require the <literal>UNSAFE:</literal> 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
+ (<literal>call-count</literal>, <literal>plugin-errors</literal>) can be
+ used without the prefix.
+ </para>
+
+ <para>
+ Examples:
+ <programlisting>
+PGOAUTHDEBUG=call-count <lineannotation>safe options only</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,trace <lineannotation>enable HTTP and traffic logging</lineannotation>
+PGOAUTHDEBUG=UNSAFE:http,call-count <lineannotation>mix of unsafe and safe</lineannotation>
+ </programlisting>
+ </para>
+
<warning>
<para>
- Do not share the output of the OAuth flow traffic with third parties. It
- contains secrets that can be used to attack your clients and servers.
+ Never use unsafe debug options in production environments. They expose
+ secrets and behaviors that can be used to attack your clients and servers.
+ Do not share <literal>trace</literal> output with third parties.
</para>
</warning>
</sect2>
diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h
index 293e9936989..dacd2dbacfe 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.h
+++ b/src/interfaces/libpq-oauth/oauth-utils.h
@@ -35,7 +35,6 @@ typedef enum
PG_BOOL_NO /* No (false) */
} PGTernaryBool;
-extern bool oauth_unsafe_debugging_enabled(void);
extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending);
extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe);
diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h
index 872f5df551a..a50d7b03408 100644
--- a/src/interfaces/libpq/fe-auth-oauth.h
+++ b/src/interfaces/libpq/fe-auth-oauth.h
@@ -40,7 +40,6 @@ typedef struct
} fe_oauth_state;
extern void pqClearOAuthToken(PGconn *conn);
-extern bool oauth_unsafe_debugging_enabled(void);
/* Mechanisms in fe-auth-oauth.c */
extern const pg_fe_sasl_mech pg_oauth_mech;
diff --git a/src/interfaces/libpq/oauth-debug.h b/src/interfaces/libpq/oauth-debug.h
new file mode 100644
index 00000000000..4f0c87117e1
--- /dev/null
+++ b/src/interfaces/libpq/oauth-debug.h
@@ -0,0 +1,142 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth-debug.h
+ * Parsing logic for PGOAUTHDEBUG environment variable
+ *
+ * Both libpq and libpq-oauth need this logic, so it's packaged in a small
+ * header for convenience. This is not quite a standalone header, due to the
+ * complication introduced by libpq_gettext(); see note below.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/oauth-debug.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef OAUTH_DEBUG_H
+#define OAUTH_DEBUG_H
+
+#include "postgres_fe.h"
+
+/*
+ * XXX libpq-oauth can't compile against libpq-int.h, so clients of this header
+ * need to provide the declaration of libpq_gettext() before #including it.
+ * Fortunately, there are only two such clients.
+ */
+/* #include "libpq-int.h" */
+
+/*
+ * Debug flags for the PGOAUTHDEBUG environment variable. Each flag controls a
+ * specific debug feature. OAUTHDEBUG_UNSAFE_* flags require the envvar to have
+ * a literal "UNSAFE:" prefix.
+ */
+
+/* allow HTTP (unencrypted) connections */
+#define OAUTHDEBUG_UNSAFE_HTTP (1<<0)
+/* log HTTP traffic (exposes secrets) */
+#define OAUTHDEBUG_UNSAFE_TRACE (1<<1)
+/* allow zero-second retry intervals */
+#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT (1<<2)
+
+/* mind the gap in values; see OAUTHDEBUG_UNSAFE_MASK below */
+
+/* print PQconnectPoll statistics */
+#define OAUTHDEBUG_CALL_COUNT (1<<16)
+/* print plugin loading errors */
+#define OAUTHDEBUG_PLUGIN_ERRORS (1<<17)
+
+/* all safe and unsafe flags, for the legacy UNSAFE behavior */
+#define OAUTHDEBUG_LEGACY_UNSAFE ((uint32) ~0)
+
+/* Flags are divided into "safe" and "unsafe" based on bit position. */
+#define OAUTHDEBUG_UNSAFE_MASK ((uint32) 0x0000FFFF)
+
+static_assert(OAUTHDEBUG_CALL_COUNT == OAUTHDEBUG_UNSAFE_MASK + 1,
+ "the first safe OAUTHDEBUG flag should be above OAUTHDEBUG_UNSAFE_MASK");
+
+/*
+ * Parses the PGOAUTHDEBUG environment variable and returns debug flags.
+ *
+ * Supported formats:
+ * PGOAUTHDEBUG=UNSAFE - legacy format, enables all features
+ * PGOAUTHDEBUG=option1,option2 - enable safe features only
+ * PGOAUTHDEBUG=UNSAFE:opt1,opt2 - enable unsafe and/or safe features
+ *
+ * Prints a warning and skips the invalid option if:
+ * - An unrecognized option is specified
+ * - An unsafe option is specified without the UNSAFE: prefix
+ *
+ * XXX The parsing, and any warnings, will happen each time the function is
+ * called, so consider caching the result in cases where that might get
+ * annoying. But don't try to cache inside this function, unless you also have a
+ * plan for getting libpq and libpq-oauth to share that cache safely... probably
+ * not worth the effort for a debugging aid?
+ */
+static uint32
+oauth_parse_debug_flags(void)
+{
+ uint32 flags = 0;
+ const char *env = getenv("PGOAUTHDEBUG");
+ char *options_str;
+ char *option;
+ char *saveptr = NULL;
+ bool unsafe_allowed = false;
+
+ if (!env || env[0] == '\0')
+ return flags;
+
+ if (strcmp(env, "UNSAFE") == 0)
+ return OAUTHDEBUG_LEGACY_UNSAFE;
+
+ if (strncmp(env, "UNSAFE:", 7) == 0)
+ {
+ unsafe_allowed = true;
+ env += 7;
+ }
+
+ options_str = strdup(env);
+ if (!options_str)
+ return flags;
+
+ option = strtok_r(options_str, ",", &saveptr);
+ while (option != NULL)
+ {
+ uint32 flag = 0;
+
+ if (strcmp(option, "http") == 0)
+ flag = OAUTHDEBUG_UNSAFE_HTTP;
+ else if (strcmp(option, "trace") == 0)
+ flag = OAUTHDEBUG_UNSAFE_TRACE;
+ else if (strcmp(option, "dos-endpoint") == 0)
+ flag = OAUTHDEBUG_UNSAFE_DOS_ENDPOINT;
+ else if (strcmp(option, "call-count") == 0)
+ flag = OAUTHDEBUG_CALL_COUNT;
+ else if (strcmp(option, "plugin-errors") == 0)
+ flag = OAUTHDEBUG_PLUGIN_ERRORS;
+ else
+ fprintf(stderr,
+ libpq_gettext("WARNING: unrecognized PGOAUTHDEBUG option \"%s\" (ignored)\n"),
+ option);
+
+ if (!unsafe_allowed && ((flag & OAUTHDEBUG_UNSAFE_MASK) != 0))
+ {
+ flag = 0;
+
+ fprintf(stderr,
+ libpq_gettext("WARNING: PGOAUTHDEBUG option \"%s\" is unsafe (ignored)\n"),
+ option);
+ }
+
+ flags |= flag;
+ option = strtok_r(NULL, ",", &saveptr);
+ }
+
+ free(options_str);
+
+ return flags;
+}
+
+#endif /* OAUTH_DEBUG_H */
diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c
index 3baede1b2e7..abbef93f95f 100644
--- a/src/interfaces/libpq-oauth/oauth-curl.c
+++ b/src/interfaces/libpq-oauth/oauth-curl.c
@@ -49,6 +49,12 @@
#endif /* USE_DYNAMIC_OAUTH */
+/*
+ * oauth-debug.h needs the declaration of libpq_gettext(), from one of the above
+ * sources.
+ */
+#include "oauth-debug.h"
+
/* One final guardrail against accidental inclusion... */
#if defined(USE_DYNAMIC_OAUTH) && defined(LIBPQ_INT_H)
#error do not rely on libpq-int.h in dynamic builds of libpq-oauth
@@ -274,7 +280,7 @@ struct async_ctx
int running; /* is asynchronous work in progress? */
bool user_prompted; /* have we already sent the authz prompt? */
bool used_basic_auth; /* did we send a client secret? */
- bool debugging; /* can we give unsafe developer assistance? */
+ uint32 debug_flags; /* can we give developer assistance? */
int dbg_num_calls; /* (debug mode) how many times were we called? */
};
@@ -1023,7 +1029,7 @@ parse_interval(struct async_ctx *actx, const char *interval_str)
parsed = ceil(parsed);
if (parsed < 1)
- return actx->debugging ? 0 : 1;
+ return (actx->debug_flags & OAUTHDEBUG_UNSAFE_DOS_ENDPOINT) ? 0 : 1;
else if (parsed >= INT_MAX)
return INT_MAX;
@@ -1797,7 +1803,7 @@ setup_curl_handles(struct async_ctx *actx)
*/
CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
- if (actx->debugging)
+ if (actx->debug_flags & OAUTHDEBUG_UNSAFE_TRACE)
{
/*
* Set a callback for retrieving error information from libcurl, the
@@ -1829,7 +1835,7 @@ setup_curl_handles(struct async_ctx *actx)
const long unsafe = CURLPROTO_HTTPS | CURLPROTO_HTTP;
#endif
- if (actx->debugging)
+ if (actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP)
protos = unsafe;
CHECK_SETOPT(actx, popt, protos, return false);
@@ -2297,7 +2303,7 @@ check_for_device_flow(struct async_ctx *actx)
* decent time to bail out if we're not using HTTPS for the endpoints
* we'll use for the flow.
*/
- if (!actx->debugging)
+ if ((actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP) == 0)
{
if (pg_strncasecmp(provider->device_authorization_endpoint,
HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0)
@@ -3027,7 +3033,7 @@ pg_fe_run_oauth_flow(PGconn *conn, struct PGoauthBearerRequest *request,
* drain_timer_events(), when we're in debug mode, track the total number
* of calls to this function and print that at the end of the flow.
*/
- if (actx->debugging)
+ if (actx->debug_flags & OAUTHDEBUG_CALL_COUNT)
{
actx->dbg_num_calls++;
if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED)
@@ -3087,8 +3093,8 @@ pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request)
* Now finish filling in the actx.
*/
- /* Should we enable unsafe features? */
- actx->debugging = oauth_unsafe_debugging_enabled();
+ /* Parse debug flags from the environment. */
+ actx->debug_flags = oauth_parse_debug_flags();
initPQExpBuffer(&actx->work_data);
initPQExpBuffer(&actx->errbuf);
diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c
index ccb0d9bf2c5..004d41f02aa 100644
--- a/src/interfaces/libpq-oauth/oauth-utils.c
+++ b/src/interfaces/libpq-oauth/oauth-utils.c
@@ -75,17 +75,6 @@ libpq_gettext(const char *msgid)
#endif /* ENABLE_NLS */
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
-
/*
* Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by
* pq_block/reset_sigpipe().
diff --git a/src/interfaces/libpq-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c
index 4328a332738..9db39053dd3 100644
--- a/src/interfaces/libpq-oauth/test-oauth-curl.c
+++ b/src/interfaces/libpq-oauth/test-oauth-curl.c
@@ -89,7 +89,7 @@ init_test_actx(void)
actx->mux = PGINVALID_SOCKET;
actx->timerfd = -1;
- actx->debugging = true;
+ actx->debug_flags = OAUTHDEBUG_LEGACY_UNSAFE;
initPQExpBuffer(&actx->errbuf);
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index ac03d1d4f9d..826f7461cb3 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -26,6 +26,7 @@
#include "fe-auth.h"
#include "fe-auth-oauth.h"
#include "mb/pg_wchar.h"
+#include "oauth-debug.h"
#include "pg_config_paths.h"
#include "utils/memdebug.h"
@@ -389,7 +390,7 @@ issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
authority_start = wkuri + strlen(HTTPS_SCHEME);
if (!authority_start
- && oauth_unsafe_debugging_enabled()
+ && (oauth_parse_debug_flags() & OAUTHDEBUG_UNSAFE_HTTP)
&& pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
{
/* Allow http:// for testing only. */
@@ -900,7 +901,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
*
* Note that POSIX dlerror() isn't guaranteed to be threadsafe.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_parse_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
return 0;
@@ -922,7 +923,7 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *re
* cause is still locked behind PGOAUTHDEBUG due to the dlerror()
* threadsafety issue.
*/
- if (oauth_unsafe_debugging_enabled())
+ if (oauth_parse_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->flow_module);
@@ -1437,17 +1438,6 @@ pqClearOAuthToken(PGconn *conn)
conn->oauth_token = NULL;
}
-/*
- * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
- */
-bool
-oauth_unsafe_debugging_enabled(void)
-{
- const char *env = getenv("PGOAUTHDEBUG");
-
- return (env && strcmp(env, "UNSAFE") == 0);
-}
-
/*
* Hook v1 Poisoning
*
diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index c9c46e63539..3d190c2ba71 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -93,6 +93,21 @@ $node->connect_fails(
qr@OAuth discovery URI "\Q$issuer\E/.well-known/openid-configuration" must use HTTPS@
);
+{
+ # PGOAUTHDEBUG=http should have no effect (it needs an UNSAFE: marker).
+ local $ENV{PGOAUTHDEBUG} = "http";
+
+ $node->connect_fails(
+ "user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
+ "HTTPS is required without debug mode (bad PGOAUTHDEBUG value)",
+ expected_stderr => qr[
+ ^WARNING: .* \Qoption "http" is unsafe\E
+ .*
+ \QOAuth discovery URI "$issuer/.well-known/openid-configuration" must use HTTPS\E
+ ]msx
+ );
+}
+
# Switch to HTTPS.
$issuer = "https://127.0.0.1:$port";
@@ -172,8 +187,11 @@ $node->connect_ok(
],
log_unlike => [qr/FATAL.*OAuth bearer authentication failed/]);
-# Enable PGOAUTHDEBUG for all remaining tests.
-$ENV{PGOAUTHDEBUG} = "UNSAFE";
+# Enable some debugging features for all remaining tests:
+# - trace, for detailed Curl logs on failure
+# - dos-endpoint, to speed up the three-way handshake
+# - call-count, for our later sanity check
+$ENV{PGOAUTHDEBUG} = "UNSAFE:trace,dos-endpoint,call-count";
# The /alternate issuer uses slightly different parameters, along with an
# OAuth-style discovery document.
diff --git a/src/tools/pginclude/headerscheck b/src/tools/pginclude/headerscheck
index 14c466cc237..de50b6937af 100755
--- a/src/tools/pginclude/headerscheck
+++ b/src/tools/pginclude/headerscheck
@@ -153,6 +153,8 @@ do
test "$f" = src/include/catalog/syscache_ids.h && continue
test "$f" = src/include/catalog/syscache_info.h && continue
+ test "$f" = src/interfaces/libpq/oauth-debug.h && continue
+
# We can't make these Bison output files compilable standalone
# without using "%code require", which old Bison versions lack.
# parser/gram.h will be included by parser/gramparse.h anyway.
--
2.34.1
^ permalink raw reply [nested|flat] 13+ messages in thread
end of thread, other threads:[~2026-04-03 17:20 UTC | newest]
Thread overview: 13+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-02-18 15:07 [oauth] Split and extend PGOAUTHDEBUG Zsolt Parragi <[email protected]>
2026-03-30 21:41 ` Jacob Champion <[email protected]>
2026-03-30 23:26 ` Jacob Champion <[email protected]>
2026-03-31 17:44 ` Zsolt Parragi <[email protected]>
2026-03-31 23:50 ` Jacob Champion <[email protected]>
2026-04-01 03:45 ` Chao Li <[email protected]>
2026-04-01 17:05 ` Jacob Champion <[email protected]>
2026-04-01 09:35 ` Zsolt Parragi <[email protected]>
2026-04-01 17:09 ` Jacob Champion <[email protected]>
2026-04-01 18:50 ` Jacob Champion <[email protected]>
2026-04-01 21:12 ` Zsolt Parragi <[email protected]>
2026-04-01 22:59 ` Jacob Champion <[email protected]>
2026-04-03 17:20 ` Jacob Champion <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox