0006-libpq-add-scram_max_iterations-connection-parameter.patch
text/x-patch
Filename: 0006-libpq-add-scram_max_iterations-connection-parameter.patch
Type: text/x-patch
Part: 5
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch 0006
Subject: libpq: add scram_max_iterations connection parameter
| File | + | − |
|---|---|---|
| doc/src/sgml/libpq.sgml | 39 | 0 |
| src/interfaces/libpq/fe-auth-scram.c | 25 | 1 |
| src/interfaces/libpq/fe-connect.c | 30 | 0 |
| src/interfaces/libpq/libpq-int.h | 3 | 0 |
| src/test/authentication/t/001_password.pl | 47 | 1 |
From 0d0f3016351ba28d11bb84563591803da5b1d26d Mon Sep 17 00:00:00 2001
From: Sehrope Sarkuni <sehrope@jackdb.com>
Date: Sat, 23 May 2026 11:16:03 -0400
Subject: [PATCH 6/6] libpq: add scram_max_iterations connection parameter
Adds a client-side hard cap on the PBKDF2 iteration count advertised by
the server during a SCRAM exchange. Complements connect_timeout's
SCRAM iteration deadline. Whereas the deadline protects callers that set
a timeout, scram_max_iterations protects all callers including ones with
no timeout configured.
The new parameter is a normal libpq connection option, so it is
accepted via connection strings, URI parameters, the PGSCRAMMAXITERATIONS
environment variable, and PQconnectdbParams() keywords.
If the server-advertised iteration count exceeds the configured limit,
the connection is aborted before any PBKDF2 work runs, with an error
identifying both the requested and the configured value. A value of 0
disables the check, preserving existing behavior. Defaults to 100K.
Includes a TAP test covering the rejection path against a doctored
verifier with a large iteration count, the accept path against a
normal verifier under a generous limit, and the disabled behavior.
---
doc/src/sgml/libpq.sgml | 39 ++++++++++++++++++
src/interfaces/libpq/fe-auth-scram.c | 26 +++++++++++-
src/interfaces/libpq/fe-connect.c | 30 ++++++++++++++
src/interfaces/libpq/libpq-int.h | 3 ++
src/test/authentication/t/001_password.pl | 48 ++++++++++++++++++++++-
5 files changed, 144 insertions(+), 2 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index af73a041762..776bdedfdc8 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1458,6 +1458,35 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</listitem>
</varlistentry>
+ <varlistentry id="libpq-connect-scram-max-iterations" xreflabel="scram_max_iterations">
+ <term><literal>scram_max_iterations</literal></term>
+ <listitem>
+ <para>
+ Maximum acceptable PBKDF2 iteration count advertised by the server
+ during a SCRAM authentication exchange. If the server proposes a
+ higher count, the connection is aborted before any PBKDF2 work is
+ performed.
+ Defaults to <literal>100000</literal>, which is above PostgreSQL's
+ server-side <xref linkend="guc-scram-iterations"/> default of
+ <literal>4096</literal> and the per-role values
+ <application>psql</application>'s
+ <command>\password</command> command produces.
+ Set to <literal>0</literal> to disable the check entirely and accept
+ any iteration count.
+ Negative values are rejected at connection-option time.
+ </para>
+ <para>
+ This caps the client-side CPU cost of SCRAM authentication
+ independently of <xref linkend="libpq-connect-connect-timeout"/>,
+ so it protects callers that connect without a timeout, and
+ applies on both the blocking and asynchronous connection paths.
+ Clients connecting to servers configured with an unusually high
+ <literal>scram_iterations</literal> may need to raise this
+ parameter, or set it to <literal>0</literal> to disable the check.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-client-encoding" xreflabel="client_encoding">
<term><literal>client_encoding</literal></term>
<listitem>
@@ -9406,6 +9435,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
</para>
</listitem>
+ <listitem>
+ <para>
+ <indexterm>
+ <primary><envar>PGSCRAMMAXITERATIONS</envar></primary>
+ </indexterm>
+ <envar>PGSCRAMMAXITERATIONS</envar> behaves the same as the <xref
+ linkend="libpq-connect-scram-max-iterations"/> connection parameter.
+ </para>
+ </listitem>
+
<listitem>
<para>
<indexterm>
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 540d9baf0b2..f63a906f571 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -698,8 +698,32 @@ read_server_first_message(fe_scram_state *state, char *input)
return false;
}
- if (*input != '\0')
+ if (*input != '\0') {
libpq_append_conn_error(conn, "malformed SCRAM message (garbage at end of server-first-message)");
+ return false;
+ }
+
+ /*
+ * Enforce a client-side hard cap on the server-advertised iteration
+ * count, if one was configured. This protects callers (including
+ * those with no connect_timeout set) from a misconfigured or hostile
+ * server forcing arbitrarily large PBKDF2 work.
+ */
+ if (conn->scram_max_iterations != NULL)
+ {
+ int max_iterations;
+
+ if (!pqParseIntParam(conn->scram_max_iterations, &max_iterations, conn,
+ "scram_max_iterations"))
+ return false;
+ if (max_iterations > 0 && state->iterations > max_iterations)
+ {
+ libpq_append_conn_error(conn,
+ "server requested SCRAM iteration count %d, exceeding scram_max_iterations (%d)",
+ state->iterations, max_iterations);
+ return false;
+ }
+ }
return true;
}
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index b766013971e..670d5abd303 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -141,6 +141,7 @@ static int ldapServiceLookup(const char *purl, PQconninfoOption *options,
#else
#define DefaultGSSMode "disable"
#endif
+#define DefaultScramMaxIterations "100000"
/* ----------
* Definition of the conninfo parameters and their fallback resources.
@@ -223,6 +224,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"Channel-Binding", "", 8, /* sizeof("require") == 8 */
offsetof(struct pg_conn, channel_binding)},
+ {"scram_max_iterations", "PGSCRAMMAXITERATIONS", DefaultScramMaxIterations, NULL,
+ "SCRAM-Max-Iterations", "", 10, /* strlen(INT32_MAX) == 10 */
+ offsetof(struct pg_conn, scram_max_iterations)},
+
{"connect_timeout", "PGCONNECT_TIMEOUT", NULL, NULL,
"Connect-timeout", "", 10, /* strlen(INT32_MAX) == 10 */
offsetof(struct pg_conn, connect_timeout)},
@@ -1763,6 +1768,30 @@ pqConnectOptions2(PGconn *conn)
goto oom_error;
}
+ /*
+ * validate scram_max_iterations option
+ */
+ if (conn->scram_max_iterations)
+ {
+ int max_iterations;
+
+ if (!pqParseIntParam(conn->scram_max_iterations, &max_iterations,
+ conn, "scram_max_iterations"))
+ {
+ conn->status = CONNECTION_BAD;
+ return false;
+ }
+ if (max_iterations < 0)
+ {
+ conn->status = CONNECTION_BAD;
+ libpq_append_conn_error(conn,
+ "invalid %s value: \"%s\" (must be zero or positive)",
+ "scram_max_iterations",
+ conn->scram_max_iterations);
+ return false;
+ }
+ }
+
#ifndef USE_SSL
/*
@@ -5114,6 +5143,7 @@ freePGconn(PGconn *conn)
free(conn->pghostaddr);
free(conn->pgport);
free(conn->connect_timeout);
+ free(conn->scram_max_iterations);
free(conn->pgtcp_user_timeout);
free(conn->client_encoding_initial);
free(conn->pgoptions);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 0f8696e032d..90991fdf3ec 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,9 @@ struct pg_conn
char *pgport; /* the server's communication port number, or
* a comma-separated list of ports */
char *connect_timeout; /* connection timeout (numeric string) */
+ char *scram_max_iterations; /* maximum acceptable server-advertised
+ * SCRAM iteration count (numeric
+ * string); 0 disables */
char *pgtcp_user_timeout; /* tcp user timeout (numeric string) */
char *client_encoding_initial; /* encoding to use */
char *pgoptions; /* options to start the backend with */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3194d1224d0..198a4894a0d 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -807,11 +807,57 @@ $node->safe_psql(
WHERE rolname = 'scram_slow';});
reset_pg_hba($node, 'all', 'all', 'scram-sha-256');
{
+ # The default scram_max_iterations cap (100000) would reject this
+ # doctored verifier before any PBKDF2 work begins. Explicitly
+ # disable the cap with scram_max_iterations=0 so the iteration
+ # loop is reached and connect_timeout has something to interrupt.
$node->connect_fails(
- "user=scram_slow connect_timeout=1",
+ "user=scram_slow connect_timeout=1 scram_max_iterations=0",
'connect_timeout aborts SCRAM iteration loop',
expected_stderr => qr/connection timeout expired/);
}
+
+# Test scram_max_iterations
+{
+ # Default rejects excessive iterations
+ $node->connect_fails(
+ "user=scram_slow",
+ 'scram_max_iterations default rejects oversized server iteration count',
+ expected_stderr =>
+ qr/server requested SCRAM iteration count 999999999, exceeding scram_max_iterations \(100000\)/
+ );
+
+ # Explicit setting equal to the default behaves the same way.
+ $node->connect_fails(
+ "user=scram_slow scram_max_iterations=100000",
+ 'scram_max_iterations rejects oversized server iteration count',
+ expected_stderr =>
+ qr/server requested SCRAM iteration count 999999999, exceeding scram_max_iterations \(100000\)/
+ );
+
+ # Accept scram iterations below scram_max_iterations.
+ $node->connect_ok(
+ "user=scram_role scram_max_iterations=100000",
+ 'scram_max_iterations accepts normal server iteration count');
+
+ # A normal verifier connects fine under the compiled-in default.
+ $node->connect_ok(
+ "user=scram_role",
+ 'scram_max_iterations default accepts normal verifier');
+
+ # Zero disables the client-side iteration-count cap.
+ $node->connect_ok(
+ "user=scram_role scram_max_iterations=0",
+ 'scram_max_iterations=0 accepts normal verifier');
+
+ # Reject negative scram_max_iterations.
+ $node->connect_fails(
+ "user=scram_role scram_max_iterations=-1",
+ 'scram_max_iterations rejects negative values',
+ expected_stderr =>
+ qr/invalid scram_max_iterations value: "-1" \(must be zero or positive\)/
+ );
+}
reset_pg_hba($node, 'all', 'all', 'trust');
$node->safe_psql('postgres', 'DROP ROLE scram_slow;');
--
2.43.0