From 760ff708bf265d9ae53d6499845735a8c99df5be Mon Sep 17 00:00:00 2001 From: Marian Koncek Date: Mon, 8 Jun 2026 15:34:58 +0200 Subject: [PATCH] Limit SCRAM PBKDF2 iterations to prevent DoS via malicious server Resolves: RHEL-173478 --- CVE-2026-42198-tests.patch | 259 +++++++++++++++++++++++++++++++++++++ CVE-2026-42198.patch | 157 ++++++++++++++++++++++ postgresql-jdbc.spec | 14 +- 3 files changed, 427 insertions(+), 3 deletions(-) create mode 100644 CVE-2026-42198-tests.patch create mode 100644 CVE-2026-42198.patch diff --git a/CVE-2026-42198-tests.patch b/CVE-2026-42198-tests.patch new file mode 100644 index 0000000..c1664c6 --- /dev/null +++ b/CVE-2026-42198-tests.patch @@ -0,0 +1,259 @@ +From cda68fb74340f5b661b0014a99c24758b6c7f20b Mon Sep 17 00:00:00 2001 +From: Marian Koncek +Date: Mon, 8 Jun 2026 14:51:26 +0200 +Subject: [PATCH 2/2] Add tests for CVE-2026-42198 + +Co-authored-by: Cursor +--- + .../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. ++ * ++ *

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. ++ * ++ *

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$:$: ++ // 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 + diff --git a/CVE-2026-42198.patch b/CVE-2026-42198.patch new file mode 100644 index 0000000..588014a --- /dev/null +++ b/CVE-2026-42198.patch @@ -0,0 +1,157 @@ +From 2f69e69103319fd336bf63194a2ebc0e602dc481 Mon Sep 17 00:00:00 2001 +From: Marian Koncek +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 +--- + 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 + diff --git a/postgresql-jdbc.spec b/postgresql-jdbc.spec index bdec0ef..32d6030 100644 --- a/postgresql-jdbc.spec +++ b/postgresql-jdbc.spec @@ -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 - 42.2.14-4 +- Limit SCRAM PBKDF2 iterations to prevent DoS via malicious server +- Resolves: CVE-2026-42198 + * Wed Feb 28 2024 Zuzana Miklankova - 42.2.14-3 - Fix CVE-2024-1597