diff --git a/CVE-2026-42198-tests.patch b/CVE-2026-42198-tests.patch new file mode 100644 index 0000000..cb0e69d --- /dev/null +++ b/CVE-2026-42198-tests.patch @@ -0,0 +1,182 @@ +From 6e318b53fa3b1b84139fa13b2f121670b884c7da Mon Sep 17 00:00:00 2001 +From: Marian Koncek +Date: Wed, 20 May 2026 12:58:33 +0200 +Subject: [PATCH] Add tests for CVE-2026-42198 + +--- + .../org/postgresql/core/ServerVersion.java | 4 +- + .../java/org/postgresql/jdbc/ScramTest.java | 91 +++++++++++++++++++ + .../java/org/postgresql/test/TestUtil.java | 15 +++ + 3 files changed, 109 insertions(+), 1 deletion(-) + +diff --git a/src/main/java/org/postgresql/core/ServerVersion.java b/src/main/java/org/postgresql/core/ServerVersion.java +index 5d18da1..4036b01 100644 +--- a/src/main/java/org/postgresql/core/ServerVersion.java ++++ b/src/main/java/org/postgresql/core/ServerVersion.java +@@ -30,7 +30,9 @@ public enum ServerVersion implements Version { + v11("11"), + v12("12"), + v13("13"), +- v14("14") ++ 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 +index 9c6e3e0..60bc75a 100644 +--- a/src/test/java/org/postgresql/jdbc/ScramTest.java ++++ b/src/test/java/org/postgresql/jdbc/ScramTest.java +@@ -11,12 +11,15 @@ 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; + +@@ -24,6 +27,7 @@ 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 { +@@ -94,6 +98,79 @@ class ScramTest { + 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'"); +@@ -102,4 +179,18 @@ class ScramTest { + } + } + ++ 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 7064f86..e89dab0 100644 +--- a/src/test/java/org/postgresql/test/TestUtil.java ++++ b/src/test/java/org/postgresql/test/TestUtil.java +@@ -975,6 +975,21 @@ public class TestUtil { + return hasNext; + } + ++ /** ++ * Execute a SQL query with a given connection, fetch the first row, and return its ++ * string value. ++ */ ++ public static /* @Nullable */ 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; ++ } ++ + /** + * Retrieve the backend process id for a given connection. + */ +-- +2.54.0 + diff --git a/CVE-2026-42198.patch b/CVE-2026-42198.patch new file mode 100644 index 0000000..3c9d8b1 --- /dev/null +++ b/CVE-2026-42198.patch @@ -0,0 +1,154 @@ +From be412561fe988dc9396ec3e492af2e8a4e43c620 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/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java +index 3f4cad67..2d6ec487 100644 +--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java ++++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java +@@ -480,6 +480,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 0ffc9e56..7732bdcb 100644 +--- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java ++++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java +@@ -754,9 +754,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, castNonNull(password), pgStream); ++ scramAuthenticator = new org.postgresql.jre7.sasl.ScramAuthenticator(user, castNonNull(password), pgStream, scramMaxIterations); + scramAuthenticator.processServerMechanismsAndInit(); + scramAuthenticator.sendScramClientFirstMessage(); + // This works as follows: +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 1dfa42c3..f1c5ec78 100644 +--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java ++++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java +@@ -1208,6 +1208,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/pgjdbc/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java b/pgjdbc/src/main/java/org/postgresql/jre7/sasl/ScramAuthenticator.java +index 59f272a0..f14b3c07 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 @@ 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 /* @Nullable */ ScramSession.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 32d0620..7491d2c 100644 --- a/gating.yaml +++ b/gating.yaml @@ -3,4 +3,7 @@ product_versions: - rhel-9 decision_context: osci_compose_gate rules: - - !PassingTestCaseRule {test_case_name: osci.brew-build./plans/smoke.functional} + - !PassingTestCaseRule {test_case_name: osci.brew-build./plans/matrix/postgresql-default.functional} + - !PassingTestCaseRule {test_case_name: osci.brew-build./plans/matrix/postgresql-15.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/plans/matrix.fmf b/plans/matrix.fmf new file mode 100644 index 0000000..247b26f --- /dev/null +++ b/plans/matrix.fmf @@ -0,0 +1,44 @@ +summary: Basic smoke test +discover: + how: fmf + url: https://gitlab.com/redhat/centos-stream/tests/postgresql-jdbc.git +execute: + how: tmt + +/postgresql-default: + prepare: + - how: install + package: + - postgresql-server + - postgresql-contrib + - postgresql-test-rpm-macros + +/postgresql-15: + prepare: + - how: shell + script: dnf -y module enable postgresql:15 + - how: install + package: + - postgresql-server + - postgresql-contrib + - postgresql-test-rpm-macros + +/postgresql-16: + prepare: + - how: shell + script: dnf -y module enable postgresql:16 + - how: install + package: + - postgresql-server + - postgresql-contrib + - postgresql-test-rpm-macros + +/postgresql-18: + prepare: + - how: shell + script: dnf -y module enable postgresql:18 + - how: install + package: + - postgresql-server + - postgresql-contrib + - postgresql-test-rpm-macros diff --git a/plans/smoke.fmf b/plans/smoke.fmf deleted file mode 100644 index e753eda..0000000 --- a/plans/smoke.fmf +++ /dev/null @@ -1,9 +0,0 @@ -summary: Basic smoke test -discover: - how: fmf - url: https://gitlab.com/redhat/centos-stream/tests/postgresql-jdbc.git -execute: - how: tmt -prepare: - - how: install - package: java-21-openjdk-headless diff --git a/postgresql-jdbc.spec b/postgresql-jdbc.spec index 261666e..d28088f 100644 --- a/postgresql-jdbc.spec +++ b/postgresql-jdbc.spec @@ -49,12 +49,16 @@ Summary: JDBC driver for PostgreSQL Name: postgresql-jdbc Version: 42.2.28 -Release: 2%{?dist} +Release: 3%{?dist} License: BSD URL: http://jdbc.postgresql.org/ 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 BuildArch: noarch @@ -103,6 +107,9 @@ This package contains the API Documentation for %{name}. mv postgresql-%{version}-jdbc-src/* . +%patch -P0 -p2 +%patch -P1 -p1 + # remove any binary libs find -type f \( -name "*.jar" -or -name "*.class" \) | xargs rm -f @@ -166,6 +173,10 @@ opts="-f" %changelog +* Thu Jun 04 2026 Marian Koncek - 42.2.28-3 +- Fix CVE-2026-42198: limit SCRAM PBKDF2 iterations to prevent DoS +- Resolves: CVE-2026-42198 + * Fri Jan 09 2026 Marian Koncek - 42.2.28-2 - Bundle shaded ongres-* dependencies