pgjdbc/pgjdbc GitHub issues and pull requests (mirror)  
help / color / mirror / Atom feed
From: 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