From 0d0f3016351ba28d11bb84563591803da5b1d26d Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni 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 + + scram_max_iterations + + + 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 100000, which is above PostgreSQL's + server-side default of + 4096 and the per-role values + psql's + \password command produces. + Set to 0 to disable the check entirely and accept + any iteration count. + Negative values are rejected at connection-option time. + + + This caps the client-side CPU cost of SCRAM authentication + independently of , + 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 + scram_iterations may need to raise this + parameter, or set it to 0 to disable the check. + + + + client_encoding @@ -9406,6 +9435,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) + + + + PGSCRAMMAXITERATIONS + + PGSCRAMMAXITERATIONS behaves the same as the connection parameter. + + + 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