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 #4137: fix: limit SCRAM PBKDF2 iterations in 42.3
Date: Tue, 02 Jun 2026 12:28:49 +0000
Message-ID: <[email protected]> (raw)
Backport the SCRAM PBKDF2 iteration cap to the 42.3 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.
Verification:
- JAVA_HOME=/Users/vlsi/Library/Java/JavaVirtualMachines/liberica-1.8.0_312 ./gradlew --quiet --no-daemon :pgjdbc:cleanTest :pgjdbc:compileTestJava
- JAVA_HOME=/Users/vlsi/Library/Java/JavaVirtualMachines/liberica-1.8.0_312 ./gradlew --quiet --no-daemon -PjdkTestVersion=8 :pgjdbc:test --tests org.postgresql.jdbc.ScramTest -x :pgjdbc-osgi-test:test
- JAVA_HOME=/Users/vlsi/Library/Java/JavaVirtualMachines/liberica-1.8.0_312 ./gradlew --quiet --no-daemon :pgjdbc:checkstyleMain :pgjdbc:checkstyleTest
- git diff --check
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cd32f85aa..40dfcbba8b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ Notable changes since version 42.0.0, read the complete [History of Changes](htt
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.
+
### Changed
### Added
diff --git a/docs/documentation/head/connect.md b/docs/documentation/head/connect.md
index c1eb81ffb0..c72d80f09d 100644
--- a/docs/documentation/head/connect.md
+++ b/docs/documentation/head/connect.md
@@ -584,6 +584,15 @@ Connection conn = DriverManager.getConnection(url);
If we quote them, then we end up sending ""colname"" to the backend instead of "colname"
which will not be found.
+* **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.
+
* **authenticationPluginClassName** == String
Fully qualified class name of the class implementing the AuthenticationPlugin interface.
diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index 797a325039..e3a62e30ff 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -527,6 +527,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 6c81e8b9b2..3754c2c1f6 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
@@ -785,6 +785,13 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
case AUTH_REQ_SASL:
LOGGER.log(Level.FINEST, " <=BE AuthenticationSASL");
+ 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 = AuthenticationPluginManager.withPassword(AuthenticationRequestType.SASL, info, password -> {
if (password == null) {
throw new PSQLException(
@@ -798,7 +805,8 @@ private void doAuthentication(PGStream pgStream, String host, String user, Prope
"The server requested SCRAM-based authentication, but the password is an empty string."),
PSQLState.CONNECTION_REJECTED);
}
- return new org.postgresql.jre7.sasl.ScramAuthenticator(user, String.valueOf(password), pgStream);
+ return new org.postgresql.jre7.sasl.ScramAuthenticator(user,
+ String.valueOf(password), pgStream, scramMaxIterations);
});
scramAuthenticator.processServerMechanismsAndInit();
scramAuthenticator.sendScramClientFirstMessage();
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 384d40fb11..f66b5e9767 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -1175,6 +1175,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 20443cfe4a..0317e9b090 100644
--- a/pgjdbc/src/test/java/org/postgresql/jdbc/ScramTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/jdbc/ScramTest.java
@@ -12,12 +12,15 @@
import static org.junit.jupiter.api.Assertions.fail;
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.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@@ -28,6 +31,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
+import java.text.NumberFormat;
import java.util.Properties;
import java.util.stream.Stream;
@@ -124,6 +128,79 @@ void testInvalidPasswords(String password, String expectedMessage) throws SQLExc
}
}
+ 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(
+ TestUtil.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 = TestUtil.queryForString(conn, "SELECT USER");
+ assertEquals(ROLE_NAME, username);
+ }
+ }
+
+ 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'");
@@ -132,4 +209,16 @@ private void createRole(String passwd) throws SQLException {
}
}
+ private void createRoleWithCustomScramIters(int iters) throws SQLException {
+ TestUtil.execute(con, "DROP ROLE IF EXISTS " + ROLE_NAME);
+ TestUtil.execute(con, "CREATE ROLE " + ROLE_NAME + " WITH LOGIN");
+ // SCRAM-SHA-256$<iter>:<salt-base64>$<StoredKey-base64>:<ServerKey-base64>
+ String encodedPassword = "SCRAM-SHA-256$" + iters
+ + ":AAAAAAAAAAAAAAAAAAAAAA=="
+ + "$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
+ + ":AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
+ TestUtil.execute(con, "UPDATE pg_authid SET rolpassword = '" + encodedPassword
+ + "' WHERE rolname = '" + ROLE_NAME + "'");
+ }
+
}
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 #4137: fix: limit SCRAM PBKDF2 iterations in 42.3
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