Skip to content

Commit fea9be6

Browse files
sehropehackorum
authored andcommitted
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.
1 parent 8710d23 commit fea9be6

5 files changed

Lines changed: 144 additions & 2 deletions

File tree

doc/src/sgml/libpq.sgml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,35 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
14581458
</listitem>
14591459
</varlistentry>
14601460

1461+
<varlistentry id="libpq-connect-scram-max-iterations" xreflabel="scram_max_iterations">
1462+
<term><literal>scram_max_iterations</literal></term>
1463+
<listitem>
1464+
<para>
1465+
Maximum acceptable PBKDF2 iteration count advertised by the server
1466+
during a SCRAM authentication exchange. If the server proposes a
1467+
higher count, the connection is aborted before any PBKDF2 work is
1468+
performed.
1469+
Defaults to <literal>100000</literal>, which is above PostgreSQL's
1470+
server-side <xref linkend="guc-scram-iterations"/> default of
1471+
<literal>4096</literal> and the per-role values
1472+
<application>psql</application>'s
1473+
<command>\password</command> command produces.
1474+
Set to <literal>0</literal> to disable the check entirely and accept
1475+
any iteration count.
1476+
Negative values are rejected at connection-option time.
1477+
</para>
1478+
<para>
1479+
This caps the client-side CPU cost of SCRAM authentication
1480+
independently of <xref linkend="libpq-connect-connect-timeout"/>,
1481+
so it protects callers that connect without a timeout, and
1482+
applies on both the blocking and asynchronous connection paths.
1483+
Clients connecting to servers configured with an unusually high
1484+
<literal>scram_iterations</literal> may need to raise this
1485+
parameter, or set it to <literal>0</literal> to disable the check.
1486+
</para>
1487+
</listitem>
1488+
</varlistentry>
1489+
14611490
<varlistentry id="libpq-connect-client-encoding" xreflabel="client_encoding">
14621491
<term><literal>client_encoding</literal></term>
14631492
<listitem>
@@ -9406,6 +9435,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
94069435
</para>
94079436
</listitem>
94089437

9438+
<listitem>
9439+
<para>
9440+
<indexterm>
9441+
<primary><envar>PGSCRAMMAXITERATIONS</envar></primary>
9442+
</indexterm>
9443+
<envar>PGSCRAMMAXITERATIONS</envar> behaves the same as the <xref
9444+
linkend="libpq-connect-scram-max-iterations"/> connection parameter.
9445+
</para>
9446+
</listitem>
9447+
94099448
<listitem>
94109449
<para>
94119450
<indexterm>

src/interfaces/libpq/fe-auth-scram.c

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,8 +698,32 @@ read_server_first_message(fe_scram_state *state, char *input)
698698
return false;
699699
}
700700

701-
if (*input != '\0')
701+
if (*input != '\0') {
702702
libpq_append_conn_error(conn, "malformed SCRAM message (garbage at end of server-first-message)");
703+
return false;
704+
}
705+
706+
/*
707+
* Enforce a client-side hard cap on the server-advertised iteration
708+
* count, if one was configured. This protects callers (including
709+
* those with no connect_timeout set) from a misconfigured or hostile
710+
* server forcing arbitrarily large PBKDF2 work.
711+
*/
712+
if (conn->scram_max_iterations != NULL)
713+
{
714+
int max_iterations;
715+
716+
if (!pqParseIntParam(conn->scram_max_iterations, &max_iterations, conn,
717+
"scram_max_iterations"))
718+
return false;
719+
if (max_iterations > 0 && state->iterations > max_iterations)
720+
{
721+
libpq_append_conn_error(conn,
722+
"server requested SCRAM iteration count %d, exceeding scram_max_iterations (%d)",
723+
state->iterations, max_iterations);
724+
return false;
725+
}
726+
}
703727

704728
return true;
705729
}

src/interfaces/libpq/fe-connect.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ static int ldapServiceLookup(const char *purl, PQconninfoOption *options,
141141
#else
142142
#define DefaultGSSMode "disable"
143143
#endif
144+
#define DefaultScramMaxIterations "100000"
144145

145146
/* ----------
146147
* Definition of the conninfo parameters and their fallback resources.
@@ -223,6 +224,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
223224
"Channel-Binding", "", 8, /* sizeof("require") == 8 */
224225
offsetof(struct pg_conn, channel_binding)},
225226

227+
{"scram_max_iterations", "PGSCRAMMAXITERATIONS", DefaultScramMaxIterations, NULL,
228+
"SCRAM-Max-Iterations", "", 10, /* strlen(INT32_MAX) == 10 */
229+
offsetof(struct pg_conn, scram_max_iterations)},
230+
226231
{"connect_timeout", "PGCONNECT_TIMEOUT", NULL, NULL,
227232
"Connect-timeout", "", 10, /* strlen(INT32_MAX) == 10 */
228233
offsetof(struct pg_conn, connect_timeout)},
@@ -1768,6 +1773,30 @@ pqConnectOptions2(PGconn *conn)
17681773
goto oom_error;
17691774
}
17701775

1776+
/*
1777+
* validate scram_max_iterations option
1778+
*/
1779+
if (conn->scram_max_iterations)
1780+
{
1781+
int max_iterations;
1782+
1783+
if (!pqParseIntParam(conn->scram_max_iterations, &max_iterations,
1784+
conn, "scram_max_iterations"))
1785+
{
1786+
conn->status = CONNECTION_BAD;
1787+
return false;
1788+
}
1789+
if (max_iterations < 0)
1790+
{
1791+
conn->status = CONNECTION_BAD;
1792+
libpq_append_conn_error(conn,
1793+
"invalid %s value: \"%s\" (must be zero or positive)",
1794+
"scram_max_iterations",
1795+
conn->scram_max_iterations);
1796+
return false;
1797+
}
1798+
}
1799+
17711800
#ifndef USE_SSL
17721801

17731802
/*
@@ -5117,6 +5146,7 @@ freePGconn(PGconn *conn)
51175146
free(conn->pghostaddr);
51185147
free(conn->pgport);
51195148
free(conn->connect_timeout);
5149+
free(conn->scram_max_iterations);
51205150
free(conn->pgtcp_user_timeout);
51215151
free(conn->client_encoding_initial);
51225152
free(conn->pgoptions);

src/interfaces/libpq/libpq-int.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ struct pg_conn
384384
char *pgport; /* the server's communication port number, or
385385
* a comma-separated list of ports */
386386
char *connect_timeout; /* connection timeout (numeric string) */
387+
char *scram_max_iterations; /* maximum acceptable server-advertised
388+
* SCRAM iteration count (numeric
389+
* string); 0 disables */
387390
char *pgtcp_user_timeout; /* tcp user timeout (numeric string) */
388391
char *client_encoding_initial; /* encoding to use */
389392
char *pgoptions; /* options to start the backend with */

src/test/authentication/t/001_password.pl

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,11 +834,57 @@ sub test_conn
834834
WHERE rolname = 'scram_slow';});
835835
reset_pg_hba($node, 'all', 'all', 'scram-sha-256');
836836
{
837+
# The default scram_max_iterations cap (100000) would reject this
838+
# doctored verifier before any PBKDF2 work begins. Explicitly
839+
# disable the cap with scram_max_iterations=0 so the iteration
840+
# loop is reached and connect_timeout has something to interrupt.
837841
$node->connect_fails(
838-
"user=scram_slow connect_timeout=1",
842+
"user=scram_slow connect_timeout=1 scram_max_iterations=0",
839843
'connect_timeout aborts SCRAM iteration loop',
840844
expected_stderr => qr/connection timeout expired/);
841845
}
846+
847+
# Test scram_max_iterations
848+
{
849+
# Default rejects excessive iterations
850+
$node->connect_fails(
851+
"user=scram_slow",
852+
'scram_max_iterations default rejects oversized server iteration count',
853+
expected_stderr =>
854+
qr/server requested SCRAM iteration count 999999999, exceeding scram_max_iterations \(100000\)/
855+
);
856+
857+
# Explicit setting equal to the default behaves the same way.
858+
$node->connect_fails(
859+
"user=scram_slow scram_max_iterations=100000",
860+
'scram_max_iterations rejects oversized server iteration count',
861+
expected_stderr =>
862+
qr/server requested SCRAM iteration count 999999999, exceeding scram_max_iterations \(100000\)/
863+
);
864+
865+
# Accept scram iterations below scram_max_iterations.
866+
$node->connect_ok(
867+
"user=scram_role scram_max_iterations=100000",
868+
'scram_max_iterations accepts normal server iteration count');
869+
870+
# A normal verifier connects fine under the compiled-in default.
871+
$node->connect_ok(
872+
"user=scram_role",
873+
'scram_max_iterations default accepts normal verifier');
874+
875+
# Zero disables the client-side iteration-count cap.
876+
$node->connect_ok(
877+
"user=scram_role scram_max_iterations=0",
878+
'scram_max_iterations=0 accepts normal verifier');
879+
880+
# Reject negative scram_max_iterations.
881+
$node->connect_fails(
882+
"user=scram_role scram_max_iterations=-1",
883+
'scram_max_iterations rejects negative values',
884+
expected_stderr =>
885+
qr/invalid scram_max_iterations value: "-1" \(must be zero or positive\)/
886+
);
887+
}
842888
reset_pg_hba($node, 'all', 'all', 'trust');
843889
$node->safe_psql('postgres', 'DROP ROLE scram_slow;');
844890

0 commit comments

Comments
 (0)