Limit SCRAM PBKDF2 iterations to prevent DoS via malicious server
Resolves: RHEL-173486
This commit is contained in:
parent
26cd4ef01f
commit
29c398204f
136
CVE-2026-42198-tests.patch
Normal file
136
CVE-2026-42198-tests.patch
Normal file
@ -0,0 +1,136 @@
|
||||
From e0cf9bfeac832b8f99d52a4030980be8e379b765 Mon Sep 17 00:00:00 2001
|
||||
From: Marian Koncek <mkoncek@redhat.com>
|
||||
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$<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(con, "UPDATE pg_authid SET rolpassword = '" + encodedPassword + "' WHERE rolname = '" + ROLE_NAME + "'");
|
||||
+ }
|
||||
}
|
||||
--
|
||||
2.54.0
|
||||
|
||||
159
CVE-2026-42198.patch
Normal file
159
CVE-2026-42198.patch
Normal file
@ -0,0 +1,159 @@
|
||||
From e2f3f886e23e76410a696ad13594387a55c6281c Mon Sep 17 00:00:00 2001
|
||||
From: Sehrope Sarkuni <sehrope@jackdb.com>
|
||||
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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 <mkoncek@redhat.com> - 42.7.2-2
|
||||
- Limit SCRAM PBKDF2 iterations to prevent DoS via malicious server
|
||||
- Resolves: CVE-2026-42198
|
||||
|
||||
* Tue Apr 07 2026 Marian Koncek <mkoncek@redhat.com> - 42.7.2-1
|
||||
- Rebase to 42.7.2
|
||||
- Resolves: CVE-2024-1597
|
||||
|
||||
Loading…
Reference in New Issue
Block a user