pgjdbc/pgjdbc GitHub issues and pull requests (mirror)
help / color / mirror / Atom feedFrom: vlsi (@vlsi) <[email protected]>
To: pgjdbc/pgjdbc <[email protected]>
Subject: [pgjdbc/pgjdbc] PR #4140: fix: limit SCRAM PBKDF2 iterations in 42.2
Date: Tue, 02 Jun 2026 13:46:46 +0000
Message-ID: <[email protected]> (raw)
Backport the SCRAM PBKDF2 iteration cap to the 42.2 release line.
This adds the scramMaxIterations connection property, rejects server-advertised SCRAM iteration counts above that cap before PBKDF2 runs, and covers the behavior in ScramTest.
42.2 predates AuthenticationPluginManager, so ConnectionFactoryImpl keeps the direct ScramAuthenticator constructor (now taking the maxIterations argument plus a non-negative-value guard). ScramTest is adapted to the 42.2 TestUtil API: execute(sql, con) argument order and a local queryForString helper.
Verification (JAVA_HOME -> liberica 1.8.0_402):
- ./gradlew --quiet --no-daemon :postgresql:compileTestJava
- ./gradlew --quiet --no-daemon :postgresql:checkstyleTest
- ./gradlew --quiet --no-daemon :postgresql:forbiddenApisMain :postgresql:forbiddenApisTest
- git diff --check
Note: :postgresql:checkstyleMain reports two pre-existing Indentation/CommentsIndentation violations in StreamWrapper.java (around the //#if preprocessor block). They are present on the release/42.2.x tip and are unrelated to this change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01e6d6319f..0bd2a3055b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Security
+- fix: Limit SCRAM PBKDF2 iterations accepted from the server.
+ pgjdbc was vulnerable to a client-side denial of service in SCRAM-SHA-256 authentication, where a malicious or compromised PostgreSQL server could specify an extremely large PBKDF2 iteration count that the driver would process before authentication completed. The driver now rejects SCRAM iteration counts above `scramMaxIterations`, which defaults to 100000.
- fix: CVE-2022-31197 Fixes SQL generated in PgResultSet.refresh() to escape column identifiers so as to prevent SQL injection.
- Previously, the column names for both key and data columns in the table were copied as-is into the generated
SQL. This allowed a malicious table with column names that include statement terminator to be parsed and
diff --git a/docs/documentation/head/connect.md b/docs/documentation/head/connect.md
index 9c8ebdf46f..616caac1a2 100644
--- a/docs/documentation/head/connect.md
+++ b/docs/documentation/head/connect.md
@@ -548,6 +548,15 @@ Connection conn = DriverManager.getConnection(url);
By default this is set to true, server error details are propagated. This may include sensitive details such as query parameters.
+* **scramMaxIterations** == int
+
+ Maximum PBKDF2 iteration count that pgjdbc will accept from the server during SCRAM authentication.
+ During SCRAM-SHA-256 authentication, the server sends the iteration count used to derive the salted password.
+ If the server advertises a value higher than `scramMaxIterations`, the driver rejects authentication before
+ starting the PBKDF2 computation.
+ This limits client CPU exposure if a malicious or compromised server sends an excessively large iteration count.
+ A value of zero disables this check.
+
<a name="unix sockets"></a>
## Unix sockets
diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index 3f4cad6724..2d6ec48755 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -480,6 +480,20 @@ public enum PGProperty {
"false",
"Enable optimization to rewrite and collapse compatible INSERT statements that are batched."),
+ /**
+ * Maximum number of PBKDF2 iterations the client will accept from the server during SCRAM
+ * authentication. If the server advertises more iterations than this value, authentication
+ * is rejected before the expensive PBKDF2 computation runs. This mitigates a denial-of-service
+ * vector where a malicious or compromised server forces the client to burn CPU on an
+ * attacker-controlled iteration count. Must be a non-negative integer. Defaults to 100000. Raise
+ * only if you know you are connecting to a trusted server that legitimately uses a higher
+ * iteration count. A value of zero disables this check.
+ */
+ SCRAM_MAX_ITERATIONS(
+ "scramMaxIterations",
+ "100000",
+ "Maximum PBKDF2 iteration count accepted from the server during SCRAM authentication. A value of zero disables this check."),
+
/**
* Socket write buffer size (SO_SNDBUF). A value of {@code -1}, which is the default, means system
* default.
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
index 0ffc9e56f5..0c2265fb0b 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
@@ -756,7 +756,14 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
LOGGER.log(Level.FINEST, " <=BE AuthenticationSASL");
//#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.1"
- scramAuthenticator = new org.postgresql.jre7.sasl.ScramAuthenticator(user, castNonNull(password), pgStream);
+ int scramMaxIterations = PGProperty.SCRAM_MAX_ITERATIONS.getInt(info);
+ if (scramMaxIterations < 0) {
+ throw new PSQLException(
+ GT.tr("{0} must be a non-negative integer, but was: {1}",
+ PGProperty.SCRAM_MAX_ITERATIONS.getName(), scramMaxIterations),
+ PSQLState.INVALID_PARAMETER_VALUE);
+ }
+ scramAuthenticator = new org.postgresql.jre7.sasl.ScramAuthenticator(user, castNonNull(password), pgStream, scramMaxIterations);
scramAuthenticator.processServerMechanismsAndInit();
scramAuthenticator.sendScramClientFirstMessage();
// This works as follows:
diff --git a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
index 1dfa42c3f5..50a6bbceff 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -1143,6 +1143,22 @@ public void setSocketFactoryArg(@Nullable String socketFactoryArg) {
PGProperty.SOCKET_FACTORY_ARG.set(properties, socketFactoryArg);
}
+ /**
+ * @return maximum PBKDF2 iteration count accepted during SCRAM authentication
+ * @see PGProperty#SCRAM_MAX_ITERATIONS
+ */
+ public int getScramMaxIterations() {
+ return PGProperty.SCRAM_MAX_ITERATIONS.getIntNoCheck(properties);
+ }
+
+ /**
+ * @param scramMaxIterations maximum PBKDF2 iteration count accepted during SCRAM authentication
+ * @see PGProperty#SCRAM_MAX_ITERATIONS
+ */
+ public void setScramMaxIterations(int scramMaxIterations) {
+ PGProperty.SCRAM_MAX_ITERATIONS.set(properties, scramMaxIterations);
+ }
+
/**
* @param replication set to 'database' for logical replication or 'true' for physical replication
* @see PGProperty#REPLICATION
diff --git a/pgjdbc/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java b/pgjdbc/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java
index 59f272a0d1..85cbd7d746 100644
--- a/pgjdbc/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java
+++ b/pgjdbc/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java
@@ -7,6 +7,7 @@
import static org.postgresql.util.internal.Nullness.castNonNull;
+import org.postgresql.PGProperty;
import org.postgresql.core.PGStream;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
@@ -34,6 +35,7 @@ public class ScramAuthenticator {
private final String user;
private final String password;
private final PGStream pgStream;
+ private final int maxIterations;
private @Nullable ScramClient scramClient;
private @Nullable ScramSession scramSession;
private @Nullable ScramSession.ClientFinalProcessor clientFinalProcessor;
@@ -50,10 +52,11 @@ private void sendAuthenticationMessage(int bodyLength, BodySender bodySender)
pgStream.flush();
}
- public ScramAuthenticator(String user, String password, PGStream pgStream) {
+ public ScramAuthenticator(String user, String password, PGStream pgStream, int maxIterations) {
this.user = user;
this.password = password;
this.pgStream = pgStream;
+ this.maxIterations = maxIterations;
}
public void processServerMechanismsAndInit() throws IOException, PSQLException {
@@ -143,6 +146,15 @@ public void processServerFirstMessage(int length) throws IOException, PSQLExcept
new Object[] { serverFirstProcessor.getSalt(), serverFirstProcessor.getIteration() }
);
}
+ int iterations = serverFirstProcessor.getIteration();
+ if (maxIterations > 0 && iterations > maxIterations) {
+ throw new PSQLException(
+ GT.tr("Server requested {0} SCRAM PBKDF2 iterations, which exceeds the "
+ + "client-side limit of {1}. If you trust this server, raise the "
+ + "{2} connection property.",
+ iterations, maxIterations, PGProperty.SCRAM_MAX_ITERATIONS.getName()),
+ PSQLState.CONNECTION_REJECTED);
+ }
clientFinalProcessor = serverFirstProcessor.clientFinalProcessor(password);
diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/ScramTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/ScramTest.java
index 9c6e3e0be9..e79aae55fc 100644
--- a/pgjdbc/src/test/java/org/postgresql/jdbc/ScramTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/jdbc/ScramTest.java
@@ -11,19 +11,24 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import org.postgresql.PGProperty;
import org.postgresql.core.ServerVersion;
import org.postgresql.test.TestUtil;
+import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.sql.Connection;
+import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
+import java.text.NumberFormat;
import java.util.Properties;
class ScramTest {
@@ -94,6 +99,86 @@ void testPasswordWithoutSpace(String passwd) throws SQLException {
assertEquals(PSQLState.INVALID_PASSWORD.getState(), ex.getSQLState());
}
+ private PSQLException scramAuthExpectingFailure(String scramMaxIterations,
+ int serverScramIterations, String password) throws SQLException {
+ createRoleWithCustomScramIters(serverScramIterations);
+ Properties props = scramProperties(password);
+ if (scramMaxIterations != null) {
+ PGProperty.SCRAM_MAX_ITERATIONS.set(props, scramMaxIterations);
+ }
+ return assertThrows(PSQLException.class,
+ () -> DriverManager.getConnection(TestUtil.getURL(), props));
+ }
+
+ @Test
+ void rejectIterationCountAboveDefaultCap() throws SQLException {
+ int serverScramIterations = 789_123_456;
+ PSQLException ex = scramAuthExpectingFailure(null, serverScramIterations, "does-not-matter");
+ assertTrue(ex.getMessage().contains("exceeds"),
+ "expected iteration-cap error, got: " + ex.getMessage());
+ assertTrue(ex.getMessage().contains("scramMaxIterations"),
+ "error should reference the connection property name, got: " + ex.getMessage());
+ NumberFormat nf = NumberFormat.getNumberInstance();
+ assertTrue(ex.getMessage().contains(nf.format(serverScramIterations)),
+ "error should include the server-supplied iteration count, got: " + ex.getMessage());
+ }
+
+ @Test
+ void rejectIterationCountAboveCustomCap() throws SQLException {
+ int scramMaxIterations = 123_456;
+ int serverScramIterations = 789_123_456;
+ PSQLException ex = scramAuthExpectingFailure(Integer.toString(scramMaxIterations),
+ serverScramIterations, "does-not-matter");
+ NumberFormat nf = NumberFormat.getNumberInstance();
+ assertTrue(ex.getMessage().contains(nf.format(scramMaxIterations)),
+ "error should include the configured cap, got: " + ex.getMessage());
+ assertTrue(ex.getMessage().contains(nf.format(serverScramIterations)),
+ "error should include the server-supplied iteration count, got: " + ex.getMessage());
+ }
+
+ @Test
+ void rejectValidCredentialsAboveCustomCap() throws SQLException {
+ String password = "t0pSecret";
+ createRole(password);
+ Properties props = scramProperties(password);
+ PGProperty.SCRAM_MAX_ITERATIONS.set(props, "1234");
+ PSQLException ex = assertThrows(PSQLException.class,
+ () -> DriverManager.getConnection(TestUtil.getURL(), props));
+ NumberFormat nf = NumberFormat.getNumberInstance();
+ assertTrue(ex.getMessage().contains(nf.format(1234)),
+ "error should include the configured cap, got: " + ex.getMessage());
+ }
+
+ @Test
+ void acceptsValidCredentialsBelowCustomCap() throws SQLException {
+ assumeTrue(TestUtil.haveMinimumServerVersion(con, ServerVersion.v16));
+ int serverScramIterations = Integer.parseInt(queryForString(con, "SHOW scram_iterations"));
+ String password = "t0pSecret";
+ createRole(password);
+ Properties props = scramProperties(password);
+ PGProperty.SCRAM_MAX_ITERATIONS.set(props, Integer.toString(serverScramIterations));
+ try (Connection conn = DriverManager.getConnection(TestUtil.getURL(), props)) {
+ String username = queryForString(conn, "SELECT USER");
+ assertEquals(ROLE_NAME, username);
+ }
+ }
+
+ private static String queryForString(Connection conn, String sql) throws SQLException {
+ try (Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql)) {
+ assertTrue(rs.next());
+ return rs.getString(1);
+ }
+ }
+
+ private Properties scramProperties(String password) {
+ Properties props = new Properties();
+ PGProperty.USER.set(props, ROLE_NAME);
+ props.setProperty("username", ROLE_NAME);
+ PGProperty.PASSWORD.set(props, password);
+ return props;
+ }
+
private void createRole(String passwd) throws SQLException {
try (Statement stmt = con.createStatement()) {
stmt.execute("SET password_encryption='scram-sha-256'");
@@ -102,4 +187,16 @@ private void createRole(String passwd) throws SQLException {
}
}
+ private void createRoleWithCustomScramIters(int iters) throws SQLException {
+ TestUtil.execute("DROP ROLE IF EXISTS " + ROLE_NAME, con);
+ TestUtil.execute("CREATE ROLE " + ROLE_NAME + " WITH LOGIN", con);
+ // SCRAM-SHA-256$<iter>:<salt-base64>$<StoredKey-base64>:<ServerKey-base64>
+ String encodedPassword = "SCRAM-SHA-256$" + iters
+ + ":AAAAAAAAAAAAAAAAAAAAAA=="
+ + "$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
+ + ":AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
+ TestUtil.execute("UPDATE pg_authid SET rolpassword = '" + encodedPassword
+ + "' WHERE rolname = '" + ROLE_NAME + "'", con);
+ }
+
}
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: github://pgjdbc/pgjdbc
Cc: [email protected], [email protected]
Subject: Re: [pgjdbc/pgjdbc] PR #4140: fix: limit SCRAM PBKDF2 iterations in 42.2
In-Reply-To: <<[email protected]>>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox