Limit SCRAM PBKDF2 iterations to prevent DoS via malicious server

Resolves: RHEL-173478
This commit is contained in:
Marian Koncek 2026-06-08 15:34:58 +02:00
parent 6ce96f894c
commit 760ff708bf
3 changed files with 427 additions and 3 deletions

259
CVE-2026-42198-tests.patch Normal file
View File

@ -0,0 +1,259 @@
From cda68fb74340f5b661b0014a99c24758b6c7f20b Mon Sep 17 00:00:00 2001
From: Marian Koncek <mkoncek@redhat.com>
Date: Mon, 8 Jun 2026 14:51:26 +0200
Subject: [PATCH 2/2] Add tests for CVE-2026-42198
Co-authored-by: Cursor <cursoragent@cursor.com>
---
.../org/postgresql/core/ServerVersion.java | 6 +-
.../java/org/postgresql/jdbc/ScramTest.java | 196 ++++++++++++++++++
.../java/org/postgresql/test/TestUtil.java | 15 ++
3 files changed, 216 insertions(+), 1 deletion(-)
create mode 100644 src/test/java/org/postgresql/jdbc/ScramTest.java
diff --git a/src/main/java/org/postgresql/core/ServerVersion.java b/src/main/java/org/postgresql/core/ServerVersion.java
index 9348ba1..e3c9bd7 100644
--- a/src/main/java/org/postgresql/core/ServerVersion.java
+++ b/src/main/java/org/postgresql/core/ServerVersion.java
@@ -26,7 +26,11 @@ public enum ServerVersion implements Version {
v9_6("9.6.0"),
v10("10"),
v11("11"),
- v12("12")
+ v12("12"),
+ v13("13"),
+ v14("14"),
+ v15("15"),
+ v16("16")
;
private final int version;
diff --git a/src/test/java/org/postgresql/jdbc/ScramTest.java b/src/test/java/org/postgresql/jdbc/ScramTest.java
new file mode 100644
index 0000000..60bc75a
--- /dev/null
+++ b/src/test/java/org/postgresql/jdbc/ScramTest.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2021, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+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.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.text.NumberFormat;
+import java.util.Properties;
+
+class ScramTest {
+
+ private static Connection con;
+ private static final String ROLE_NAME = "testscram";
+
+ @BeforeAll
+ public static void setUp() throws Exception {
+ con = TestUtil.openPrivilegedDB();
+ assumeTrue(TestUtil.haveMinimumServerVersion(con, ServerVersion.v10));
+ }
+
+ @AfterAll
+ public static void tearDown() throws Exception {
+ try (Statement stmt = con.createStatement()) {
+ stmt.execute("DROP ROLE IF EXISTS " + ROLE_NAME);
+ }
+ TestUtil.closeDB(con);
+ }
+
+ /**
+ * Test creating a role with passwords WITH spaces and opening a connection using the same
+ * password, should work because is the "same" password.
+ *
+ * <p>https://github.com/pgjdbc/pgjdbc/issues/1970
+ */
+ @ParameterizedTest
+ @ValueSource(strings = {"My Space", "$ec ret", " rover june spelling ",
+ "!zj5hs*k5 STj@DaRUy", "q\u00A0w\u2000e\u2003r\u2009t\u3000y"})
+ void testPasswordWithSpace(String passwd) throws SQLException {
+ createRole(passwd); // Create role password with spaces.
+
+ Properties props = new Properties();
+ props.setProperty("username", ROLE_NAME);
+ props.setProperty("password", passwd);
+
+ try (Connection c = assertDoesNotThrow(() -> TestUtil.openDB(props));
+ Statement stmt = c.createStatement();
+ ResultSet rs = stmt.executeQuery("SELECT current_user")) {
+ assertTrue(rs.next());
+ assertEquals(ROLE_NAME, rs.getString(1));
+ }
+ }
+
+ /**
+ * Test creating a role with passwords WITHOUT spaces and opening a connection using password with
+ * spaces should fail since the spaces should not be stripped out.
+ *
+ * <p>https://github.com/pgjdbc/pgjdbc/issues/2000
+ */
+ @ParameterizedTest
+ @ValueSource(strings = {"My Space", "$ec ret", "rover june spelling",
+ "!zj5hs*k5 STj@DaRUy", "q\u00A0w\u2000e\u2003r\u2009t\u3000y"})
+ void testPasswordWithoutSpace(String passwd) throws SQLException {
+ String passwdNoSpaces = passwd.codePoints()
+ .filter(i -> !Character.isSpaceChar(i))
+ .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+ .toString();
+
+ createRole(passwdNoSpaces); // Create role password without spaces.
+
+ Properties props = new Properties();
+ props.setProperty("username", ROLE_NAME);
+ props.setProperty("password", passwd); // Open connection with spaces
+
+ SQLException ex = assertThrows(SQLException.class, () -> TestUtil.openDB(props));
+ assertEquals(PSQLState.INVALID_PASSWORD.getState(), ex.getSQLState());
+ }
+
+ private PSQLException scramAuthExpectingFailure(String scramMaxIterations, int serverScramIterations, String password) throws SQLException {
+ createRoleWithCustomScramIters(serverScramIterations);
+ Properties props = new Properties();
+ props.setProperty("username", ROLE_NAME);
+ props.setProperty("password", password);
+ if (scramMaxIterations != null) {
+ PGProperty.SCRAM_MAX_ITERATIONS.set(props, scramMaxIterations);
+ }
+ return assertThrows(PSQLException.class, () -> TestUtil.openDB(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());
+ // The message is formatted through MessageFormat, which applies locale-aware grouping
+ // to integer arguments; format the expected numbers the same way.
+ NumberFormat nf = NumberFormat.getNumberInstance();
+ assertTrue(ex.getMessage().contains(nf.format(serverScramIterations)),
+ "error should include the configured cap, 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");
+ // The message is formatted through MessageFormat, which applies locale-aware grouping
+ // to integer arguments; format the expected numbers the same way.
+ 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 = new Properties();
+ props.setProperty("username", ROLE_NAME);
+ props.setProperty("password", password);
+ PGProperty.SCRAM_MAX_ITERATIONS.set(props, "1234");
+ PSQLException ex = assertThrows(PSQLException.class, () -> TestUtil.openDB(props));
+ // The message is formatted through MessageFormat, which applies locale-aware grouping
+ // to integer arguments; format the expected numbers the same way.
+ 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),
+ "scram_iterations configuration requires PostgreSQL 16+");
+ int serverScramIterations = Integer.parseInt(TestUtil.queryForString(con, "SHOW scram_iterations"));
+ String password = "t0pSecret";
+ createRole(password);
+ Properties props = new Properties();
+ props.setProperty("username", ROLE_NAME);
+ props.setProperty("password", password);
+ PGProperty.SCRAM_MAX_ITERATIONS.set(props, Integer.toString(serverScramIterations));
+ try (Connection conn = TestUtil.openDB(props)) {
+ String username = TestUtil.queryForString(conn, "SELECT USER");
+ assertEquals(ROLE_NAME, username);
+ }
+ }
+
+ private void createRole(String passwd) throws SQLException {
+ try (Statement stmt = con.createStatement()) {
+ stmt.execute("SET password_encryption='scram-sha-256'");
+ stmt.execute("DROP ROLE IF EXISTS " + ROLE_NAME);
+ stmt.execute("CREATE ROLE " + ROLE_NAME + " WITH LOGIN PASSWORD '" + passwd + "'");
+ }
+ }
+
+ private static 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>
+ // salt: 16 zero bytes, StoredKey and ServerKey: 32 zero bytes each.
+ String encodedPassword = "SCRAM-SHA-256$" + iters
+ + ":AAAAAAAAAAAAAAAAAAAAAA=="
+ + "$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
+ + ":AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
+ // NOTE: We must directly update the system catalog to prevent the server from trying to
+ // verify the password at creation time. Otherwise it will try to hash empty string with
+ // our huge number of iterations to ensure the password is not an empty string.
+ TestUtil.execute("UPDATE pg_authid SET rolpassword = '" + encodedPassword + "' WHERE rolname = '" + ROLE_NAME + "'", con);
+ }
+}
diff --git a/src/test/java/org/postgresql/test/TestUtil.java b/src/test/java/org/postgresql/test/TestUtil.java
index 0ce472c..a7fe975 100644
--- a/src/test/java/org/postgresql/test/TestUtil.java
+++ b/src/test/java/org/postgresql/test/TestUtil.java
@@ -1020,4 +1020,19 @@ public class TestUtil {
}
}
}
+
+ /**
+ * Execute a SQL query with a given connection, fetch the first row, and return its
+ * string value.
+ */
+ public static String queryForString(Connection conn, String sql) throws SQLException {
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery(sql);
+ Assert.assertTrue("Query should have returned exactly one row but none was found: " + sql, rs.next());
+ String value = rs.getString(1);
+ Assert.assertFalse("Query should have returned exactly one row but more than one found: " + sql, rs.next());
+ rs.close();
+ stmt.close();
+ return value;
+ }
}
--
2.54.0

157
CVE-2026-42198.patch Normal file
View File

@ -0,0 +1,157 @@
From 2f69e69103319fd336bf63194a2ebc0e602dc481 Mon Sep 17 00:00:00 2001
From: Marian Koncek <mkoncek@redhat.com>
Date: Mon, 8 Jun 2026 14:50:02 +0200
Subject: [PATCH 1/2] fix: Limit SCRAM PBKDF2 iterations accepted from the
server
A malicious or compromised PostgreSQL server can advertise an
arbitrarily large PBKDF2 iteration count in its SCRAM
server-first-message, forcing the client to burn CPU inside
clientFinalMessage() before authentication can possibly fail. Combined
with an abandoned connect-thread on loginTimeout expiry, that CPU
continues spinning after the caller has given up.
We add a new scramMaxIterations connection property (default 100000)
and validate the iteration count from ServerFirstMessage against it
after parsing but before the PBKDF2-heavy clientFinalMessage() step.
Exceeding the cap throws a PSQLException with CONNECTION_REJECTED and
an error message naming the property so operators can raise it for
trusted servers that legitimately use a higher count.
Fixes CVE-2026-42198
Co-authored-by: Cursor <cursoragent@cursor.com>
---
src/main/java/org/postgresql/PGProperty.java | 14 ++++++++++++++
.../core/v3/ConnectionFactoryImpl.java | 9 ++++++++-
.../org/postgresql/ds/common/BaseDataSource.java | 16 ++++++++++++++++
.../postgresql/jre7/sasl/ScramAuthenticator.java | 15 ++++++++++++++-
4 files changed, 52 insertions(+), 2 deletions(-)
diff --git a/src/main/java/org/postgresql/PGProperty.java b/src/main/java/org/postgresql/PGProperty.java
index f94b387..712ece6 100644
--- a/src/main/java/org/postgresql/PGProperty.java
+++ b/src/main/java/org/postgresql/PGProperty.java
@@ -470,6 +470,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/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java b/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
index 9737a82..a21c0b1 100644
--- a/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
+++ b/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
@@ -666,9 +666,16 @@ public class ConnectionFactoryImpl extends ConnectionFactory {
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);
+ }
//#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.1"
- scramAuthenticator = new org.postgresql.jre7.sasl.ScramAuthenticator(user, password, pgStream);
+ scramAuthenticator = new org.postgresql.jre7.sasl.ScramAuthenticator(user, password, pgStream, scramMaxIterations);
scramAuthenticator.processServerMechanismsAndInit();
scramAuthenticator.sendScramClientFirstMessage();
// This works as follows:
diff --git a/src/main/java/org/postgresql/ds/common/BaseDataSource.java b/src/main/java/org/postgresql/ds/common/BaseDataSource.java
index e4e2bb3..522d81c 100644
--- a/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -1185,6 +1185,22 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
PGProperty.LOGGER_FILE.set(properties, loggerFile);
}
+ /**
+ * @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);
+ }
+
/**
* Generates a {@link DriverManager} URL from the other properties supplied.
*
diff --git a/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java b/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java
index 2d97387..ea78189 100644
--- a/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java
+++ b/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java
@@ -5,6 +5,7 @@
package org.postgresql.jre7.sasl;
+import org.postgresql.PGProperty;
import org.postgresql.core.PGStream;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
@@ -31,6 +32,7 @@ public class ScramAuthenticator {
private final String user;
private final String password;
private final PGStream pgStream;
+ private final int maxIterations;
private ScramClient scramClient;
private ScramSession scramSession;
private ScramSession.ServerFirstProcessor serverFirstProcessor;
@@ -48,10 +50,11 @@ public class ScramAuthenticator {
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 {
@@ -129,6 +132,16 @@ public class ScramAuthenticator {
);
}
+ 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);
String clientFinalMessage = clientFinalProcessor.clientFinalMessage();
--
2.54.0

View File

@ -31,20 +31,22 @@
Summary: JDBC driver for PostgreSQL
Name: postgresql-jdbc
Version: 42.2.14
Release: 3%{?dist}
Release: 4%{?dist}
License: BSD
URL: http://jdbc.postgresql.org/
Source0: https://repo1.maven.org/maven2/org/postgresql/postgresql/%{version}/postgresql-%{version}-src.tar.gz
Patch0: postgresql-jdbc-CVE-2022-41946.patch
Patch1: postgresql-jdbc-CVE-2024-1597.patch
Patch2: CVE-2026-42198.patch
Patch3: CVE-2026-42198-tests.patch
Provides: pgjdbc = %version-%release
BuildArch: noarch
BuildRequires: maven-local
BuildRequires: java-comment-preprocessor
BuildRequires: properties-maven-plugin
# BuildRequires: java-comment-preprocessor
# BuildRequires: properties-maven-plugin
BuildRequires: maven-enforcer-plugin
BuildRequires: maven-plugin-bundle
BuildRequires: maven-plugin-build-helper
@ -69,6 +71,8 @@ This package contains the API Documentation for %{name}.
%setup -c -q
%patch -P 0 -p1
%patch -P 1 -p2
%patch -P 2 -p1
%patch -P 3 -p1
# remove any binary libs
find -type f \( -name "*.jar" -or -name "*.class" \) | xargs rm -f
@ -107,6 +111,10 @@ find -type f \( -name "*.jar" -or -name "*.class" \) | xargs rm -f
%changelog
* Mon Jun 08 2026 Marian Koncek <mkoncek@redhat.com> - 42.2.14-4
- Limit SCRAM PBKDF2 iterations to prevent DoS via malicious server
- Resolves: CVE-2026-42198
* Wed Feb 28 2024 Zuzana Miklankova <zmiklank@redhat.com> - 42.2.14-3
- Fix CVE-2024-1597