diff --git a/CVE-2026-42198-tests.patch b/CVE-2026-42198-tests.patch new file mode 100644 index 0000000..008612e --- /dev/null +++ b/CVE-2026-42198-tests.patch @@ -0,0 +1,136 @@ +From e0cf9bfeac832b8f99d52a4030980be8e379b765 Mon Sep 17 00:00:00 2001 +From: Marian Koncek +Date: Wed, 20 May 2026 13:24:31 +0200 +Subject: [PATCH] Add tests for CVE-2026-42198 + +--- + .../java/org/postgresql/jdbc/ScramTest.java | 90 +++++++++++++++++++ + 1 file changed, 90 insertions(+) + +diff --git a/src/test/java/org/postgresql/jdbc/ScramTest.java b/src/test/java/org/postgresql/jdbc/ScramTest.java +index c67b778..77880af 100644 +--- a/src/test/java/org/postgresql/jdbc/ScramTest.java ++++ b/src/test/java/org/postgresql/jdbc/ScramTest.java +@@ -15,10 +15,12 @@ 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; +@@ -29,6 +31,7 @@ import java.sql.DriverManager; + 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; + +@@ -125,6 +128,79 @@ class ScramTest { + } + } + ++ private PSQLException scramAuthExpectingFailure(String scramMaxIterations, int serverScramIterations, String password) throws SQLException { ++ createRoleWithCustomScramIters(serverScramIterations); ++ Properties props = new Properties(); ++ PGProperty.USER.set(props, ROLE_NAME); ++ PGProperty.PASSWORD.set(props, 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(); ++ PGProperty.USER.set(props, ROLE_NAME); ++ PGProperty.PASSWORD.set(props, 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(); ++ PGProperty.USER.set(props, ROLE_NAME); ++ PGProperty.PASSWORD.set(props, 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'"); +@@ -133,4 +209,18 @@ class ScramTest { + } + } + ++ private static 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$:$: ++ // 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(con, "UPDATE pg_authid SET rolpassword = '" + encodedPassword + "' WHERE rolname = '" + ROLE_NAME + "'"); ++ } + } +-- +2.54.0 + diff --git a/CVE-2026-42198.patch b/CVE-2026-42198.patch new file mode 100644 index 0000000..e464108 --- /dev/null +++ b/CVE-2026-42198.patch @@ -0,0 +1,159 @@ +From e2f3f886e23e76410a696ad13594387a55c6281c Mon Sep 17 00:00:00 2001 +From: Sehrope Sarkuni +Date: Sat, 18 Apr 2026 08:20:17 -0400 +Subject: [PATCH] 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 +--- + .../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 da11f66d..7f7b6e00 100644 +--- a/src/main/java/org/postgresql/PGProperty.java ++++ b/src/main/java/org/postgresql/PGProperty.java +@@ -554,6 +554,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 bc0af8e4..9fbe7eba 100644 +--- a/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java ++++ b/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java +@@ -836,6 +836,13 @@ 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); ++ } + + scramAuthenticator = AuthenticationPluginManager.withPassword(AuthenticationRequestType.SASL, info, password -> { + if (password == null) { +@@ -850,7 +857,7 @@ public class ConnectionFactoryImpl extends ConnectionFactory { + "The server requested SCRAM-based authentication, but the password is an empty string."), + PSQLState.CONNECTION_REJECTED); + } +- return new ScramAuthenticator(user, String.valueOf(password), pgStream); ++ return new ScramAuthenticator(user, String.valueOf(password), pgStream, scramMaxIterations); + }); + scramAuthenticator.processServerMechanismsAndInit(); + scramAuthenticator.sendScramClientFirstMessage(); +diff --git a/src/main/java/org/postgresql/ds/common/BaseDataSource.java b/src/main/java/org/postgresql/ds/common/BaseDataSource.java +index c3b606fa..6416bd97 100644 +--- a/src/main/java/org/postgresql/ds/common/BaseDataSource.java ++++ b/src/main/java/org/postgresql/ds/common/BaseDataSource.java +@@ -1325,6 +1325,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 ff96106a..05fc2d19 100644 +--- a/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java ++++ b/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java +@@ -7,6 +7,7 @@ package org.postgresql.jre7.sasl; + + 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 ScramSession./* @Nullable */ ClientFinalProcessor clientFinalProcessor; +@@ -50,10 +52,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 { +@@ -144,6 +147,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.52.0 + diff --git a/gating.yaml b/gating.yaml index 9e8ec58..1263cbb 100644 --- a/gating.yaml +++ b/gating.yaml @@ -5,4 +5,5 @@ decision_contexts: - osci_compose_gate rules: - !PassingTestCaseRule {test_case_name: osci.brew-build./plans/javapackages.functional} - - !PassingTestCaseRule {test_case_name: osci.brew-build./plans/smoke.functional} + - !PassingTestCaseRule {test_case_name: osci.brew-build./plans/matrix/postgresql-16.functional} + - !PassingTestCaseRule {test_case_name: osci.brew-build./plans/matrix/postgresql-18.functional} diff --git a/postgresql-jdbc.spec b/postgresql-jdbc.spec index 93df0e8..bb403c6 100644 --- a/postgresql-jdbc.spec +++ b/postgresql-jdbc.spec @@ -49,13 +49,18 @@ Summary: JDBC driver for PostgreSQL Name: postgresql-jdbc Version: 42.7.2 -Release: 1%{?dist} +Release: 2%{?dist} License: BSD-2-Clause URL: https://jdbc.postgresql.org/ -Source0: https://repo1.maven.org/maven2/org/postgresql/postgresql/%{version}/postgresql-%{version}-jdbc-src.tar.gz BuildArch: noarch ExclusiveArch: %{java_arches} noarch +Source0: https://repo1.maven.org/maven2/org/postgresql/postgresql/%{version}/postgresql-%{version}-jdbc-src.tar.gz + +# https://github.com/pgjdbc/pgjdbc/commit/c9d41d1332a7426fcef19ff89f2e6b1116429143 +Patch0: CVE-2026-42198.patch +Patch1: CVE-2026-42198-tests.patch + Provides: pgjdbc = %version-%release BuildRequires: maven-local @@ -97,6 +102,9 @@ This package contains the API Documentation for %{name}. mv postgresql-%{version}-jdbc-src/* . +%patch -P0 -p1 +%patch -P1 -p1 + # remove any binary libs find -type f \( -name "*.jar" -or -name "*.class" \) | xargs rm -f @@ -166,6 +174,10 @@ opts="-f" %license LICENSE %changelog +* Thu Jun 04 2026 Marian Koncek - 42.7.2-2 +- Limit SCRAM PBKDF2 iterations to prevent DoS via malicious server +- Resolves: CVE-2026-42198 + * Tue Apr 07 2026 Marian Koncek - 42.7.2-1 - Rebase to 42.7.2 - Resolves: CVE-2024-1597