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

