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