Version in base suite: 42.5.4-1 Base version: libpgjava_42.5.4-1 Target version: libpgjava_42.5.5-0+deb12u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/libp/libpgjava/libpgjava_42.5.4-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/libp/libpgjava/libpgjava_42.5.5-0+deb12u1.dsc debian/changelog | 8 pom.xml | 2 src/main/java/org/postgresql/core/v3/SimpleParameterList.java | 149 ++++++---- src/main/java/org/postgresql/util/DriverInfo.java | 4 src/main/resources/META-INF/MANIFEST.MF | 10 src/test/java/org/postgresql/core/v3/V3ParameterListTests.java | 6 src/test/java/org/postgresql/jdbc/ParameterInjectionTest.java | 144 +++++++++ 7 files changed, 265 insertions(+), 58 deletions(-) diff -Nru libpgjava-42.5.4/debian/changelog libpgjava-42.5.5/debian/changelog --- libpgjava-42.5.4/debian/changelog 2023-02-17 17:19:35.000000000 +0000 +++ libpgjava-42.5.5/debian/changelog 2024-12-18 19:11:06.000000000 +0000 @@ -1,3 +1,11 @@ +libpgjava (42.5.5-0+deb12u1) bookworm; urgency=medium + + * Non-maintainer upload. + * New upstream release. + - CVE-2024-1597: SQL Injection via line comment generation + + -- Adrian Bunk Wed, 18 Dec 2024 21:11:06 +0200 + libpgjava (42.5.4-1) unstable; urgency=medium * New upstream version 42.5.4. diff -Nru libpgjava-42.5.4/pom.xml libpgjava-42.5.5/pom.xml --- libpgjava-42.5.4/pom.xml 1970-01-02 00:00:00.000000000 +0000 +++ libpgjava-42.5.5/pom.xml 1970-01-02 00:00:00.000000000 +0000 @@ -10,7 +10,7 @@ postgresql jar PostgreSQL JDBC Driver - JDBC 4.2 - 42.5.4 + 42.5.5 Java JDBC 4.2 (JRE 8+) driver for PostgreSQL database https://github.com/pgjdbc/pgjdbc diff -Nru libpgjava-42.5.4/src/main/java/org/postgresql/core/v3/SimpleParameterList.java libpgjava-42.5.5/src/main/java/org/postgresql/core/v3/SimpleParameterList.java --- libpgjava-42.5.4/src/main/java/org/postgresql/core/v3/SimpleParameterList.java 1970-01-02 00:00:00.000000000 +0000 +++ libpgjava-42.5.5/src/main/java/org/postgresql/core/v3/SimpleParameterList.java 1970-01-02 00:00:00.000000000 +0000 @@ -173,6 +173,59 @@ bind(index, NULL_OBJECT, oid, binaryTransfer); } + /** + *

Escapes a given text value as a literal, wraps it in single quotes, casts it to the + * to the given data type, and finally wraps the whole thing in parentheses.

+ * + *

For example, "123" and "int4" becomes "('123'::int)"

+ * + *

The additional parentheses is added to ensure that the surrounding text of where the + * parameter value is entered does modify the interpretation of the value.

+ * + *

For example if our input SQL is: SELECT ?b

+ * + *

Using a parameter value of '{}' and type of json we'd get:

+ * + *
+   * test=# SELECT ('{}'::json)b;
+   *  b
+   * ----
+   *  {}
+   * 
+ * + *

But without the parentheses the result changes:

+ * + *
+   * test=# SELECT '{}'::jsonb;
+   * jsonb
+   * -------
+   * {}
+   * 
+ **/ + private static String quoteAndCast(String text, /* @Nullable */ String type, boolean standardConformingStrings) { + + StringBuilder sb = new StringBuilder((text.length() + 10) / 10 * 11); // Add 10% for escaping. + sb.append("('"); + try { + Utils.escapeLiteral(sb, text, standardConformingStrings); + } catch (SQLException e) { + // This should only happen if we have an embedded null + // and there's not much we can do if we do hit one. + // + // To force a server side failure, we deliberately include + // a zero byte character in the literal to force the server + // to reject the command. + sb.append('\u0000'); + } + sb.append("'"); + if (type != null) { + sb.append("::"); + sb.append(type); + } + sb.append(")"); + return sb.toString(); + } + @Override public String toString(/* @Positive */ int index, boolean standardConformingStrings) { --index; @@ -180,101 +233,103 @@ if (paramValue == null) { return "?"; } else if (paramValue == NULL_OBJECT) { - return "NULL"; - } else if ((flags[index] & BINARY) == BINARY) { + return "(NULL)"; + } + String textValue; + String type; + if ((flags[index] & BINARY) == BINARY) { // handle some of the numeric types - switch (paramTypes[index]) { case Oid.INT2: short s = ByteConverter.int2((byte[]) paramValue, 0); - return Short.toString(s); + textValue = Short.toString(s); + type = "int2"; + break; case Oid.INT4: int i = ByteConverter.int4((byte[]) paramValue, 0); - return Integer.toString(i); + textValue = Integer.toString(i); + type = "int4"; + break; case Oid.INT8: long l = ByteConverter.int8((byte[]) paramValue, 0); - return Long.toString(l); + textValue = Long.toString(l); + type = "int8"; + break; case Oid.FLOAT4: float f = ByteConverter.float4((byte[]) paramValue, 0); if (Float.isNaN(f)) { - return "'NaN'::real"; + return "('NaN'::real)"; } - return Float.toString(f); + textValue = Float.toString(f); + type = "real"; + break; case Oid.FLOAT8: double d = ByteConverter.float8((byte[]) paramValue, 0); if (Double.isNaN(d)) { - return "'NaN'::double precision"; + return "('NaN'::double precision)"; } - return Double.toString(d); + textValue = Double.toString(d); + type = "double precision"; + break; case Oid.NUMERIC: Number n = ByteConverter.numeric((byte[]) paramValue); if (n instanceof Double) { assert ((Double) n).isNaN(); - return "'NaN'::numeric"; + return "('NaN'::numeric)"; } - return n.toString(); + textValue = n.toString(); + type = "numeric"; + break; case Oid.UUID: - String uuid = + textValue = new UUIDArrayAssistant().buildElement((byte[]) paramValue, 0, 16).toString(); - return "'" + uuid + "'::uuid"; - + type = "uuid"; + break; case Oid.POINT: PGpoint pgPoint = new PGpoint(); pgPoint.setByteValue((byte[]) paramValue, 0); - return "'" + pgPoint.toString() + "'::point"; + textValue = pgPoint.toString(); + type = "point"; + break; case Oid.BOX: PGbox pgBox = new PGbox(); pgBox.setByteValue((byte[]) paramValue, 0); - return "'" + pgBox.toString() + "'::box"; - } - return "?"; - } else { - String param = paramValue.toString(); - - // add room for quotes + potential escaping. - StringBuilder p = new StringBuilder(3 + (param.length() + 10) / 10 * 11); + textValue = pgBox.toString(); + type = "box"; + break; - // No E'..' here since escapeLiteral escapes all things and it does not use \123 kind of - // escape codes - p.append('\''); - try { - p = Utils.escapeLiteral(p, param, standardConformingStrings); - } catch (SQLException sqle) { - // This should only happen if we have an embedded null - // and there's not much we can do if we do hit one. - // - // The goal of toString isn't to be sent to the server, - // so we aren't 100% accurate (see StreamWrapper), put - // the unescaped version of the data. - // - p.append(param); + default: + return "?"; } - p.append('\''); + } else { + textValue = paramValue.toString(); int paramType = paramTypes[index]; if (paramType == Oid.TIMESTAMP) { - p.append("::timestamp"); + type = "timestamp"; } else if (paramType == Oid.TIMESTAMPTZ) { - p.append("::timestamp with time zone"); + type = "timestamp with time zone"; } else if (paramType == Oid.TIME) { - p.append("::time"); + type = "time"; } else if (paramType == Oid.TIMETZ) { - p.append("::time with time zone"); + type = "time with time zone"; } else if (paramType == Oid.DATE) { - p.append("::date"); + type = "date"; } else if (paramType == Oid.INTERVAL) { - p.append("::interval"); + type = "interval"; } else if (paramType == Oid.NUMERIC) { - p.append("::numeric"); + type = "numeric"; + } else { + type = null; } - return p.toString(); } + return quoteAndCast(textValue, type, standardConformingStrings); } @Override diff -Nru libpgjava-42.5.4/src/main/java/org/postgresql/util/DriverInfo.java libpgjava-42.5.5/src/main/java/org/postgresql/util/DriverInfo.java --- libpgjava-42.5.4/src/main/java/org/postgresql/util/DriverInfo.java 1970-01-02 00:00:00.000000000 +0000 +++ libpgjava-42.5.5/src/main/java/org/postgresql/util/DriverInfo.java 1970-01-02 00:00:00.000000000 +0000 @@ -16,13 +16,13 @@ // Driver name public static final String DRIVER_NAME = "PostgreSQL JDBC Driver"; public static final String DRIVER_SHORT_NAME = "PgJDBC"; - public static final String DRIVER_VERSION = "42.5.4"; + public static final String DRIVER_VERSION = "42.5.5"; public static final String DRIVER_FULL_NAME = DRIVER_NAME + " " + DRIVER_VERSION; // Driver version public static final int MAJOR_VERSION = 42; public static final int MINOR_VERSION = 5; - public static final int PATCH_VERSION = 4; + public static final int PATCH_VERSION = 5; // JDBC specification public static final String JDBC_VERSION = "4.2"; diff -Nru libpgjava-42.5.4/src/main/resources/META-INF/MANIFEST.MF libpgjava-42.5.5/src/main/resources/META-INF/MANIFEST.MF --- libpgjava-42.5.4/src/main/resources/META-INF/MANIFEST.MF 1970-01-02 00:00:00.000000000 +0000 +++ libpgjava-42.5.5/src/main/resources/META-INF/MANIFEST.MF 1970-01-02 00:00:00.000000000 +0000 @@ -1,12 +1,12 @@ Manifest-Version: 1.0 -Implementation-Title: PostgreSQL JDBC Driver Bundle-License: BSD-2-Clause -Automatic-Module-Name: org.postgresql.jdbc -Implementation-Version: 42.5.4 +Implementation-Title: PostgreSQL JDBC Driver +Implementation-Version: 42.5.5 Specification-Vendor: Oracle Corporation -Specification-Title: JDBC -Implementation-Vendor-Id: org.postgresql Specification-Version: 4.2 +Specification-Title: JDBC Implementation-Vendor: PostgreSQL Global Development Group +Implementation-Vendor-Id: org.postgresql Main-Class: org.postgresql.util.PGJDBCMain +Automatic-Module-Name: org.postgresql.jdbc diff -Nru libpgjava-42.5.4/src/test/java/org/postgresql/core/v3/V3ParameterListTests.java libpgjava-42.5.5/src/test/java/org/postgresql/core/v3/V3ParameterListTests.java --- libpgjava-42.5.4/src/test/java/org/postgresql/core/v3/V3ParameterListTests.java 1970-01-02 00:00:00.000000000 +0000 +++ libpgjava-42.5.5/src/test/java/org/postgresql/core/v3/V3ParameterListTests.java 1970-01-02 00:00:00.000000000 +0000 @@ -58,8 +58,8 @@ s2SPL.setIntParameter(4, 8); s1SPL.appendAll(s2SPL); - assertEquals( - "Expected string representation of values does not match outcome.", - "<[1 ,2 ,3 ,4 ,5 ,6 ,7 ,8]>", s1SPL.toString()); + assertEquals("Expected string representation of values does not match outcome.", + "<[('1'::int4) ,('2'::int4) ,('3'::int4) ,('4'::int4) ,('5'::int4) ,('6'::int4) ,('7'::int4) ,('8'::int4)]>", s1SPL.toString()); + } } diff -Nru libpgjava-42.5.4/src/test/java/org/postgresql/jdbc/ParameterInjectionTest.java libpgjava-42.5.5/src/test/java/org/postgresql/jdbc/ParameterInjectionTest.java --- libpgjava-42.5.4/src/test/java/org/postgresql/jdbc/ParameterInjectionTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libpgjava-42.5.5/src/test/java/org/postgresql/jdbc/ParameterInjectionTest.java 1970-01-02 00:00:00.000000000 +0000 @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2024, 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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.postgresql.test.TestUtil; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class ParameterInjectionTest { + private interface ParameterBinder { + void bind(PreparedStatement stmt) throws SQLException; + } + + private void testParamInjection(ParameterBinder bindPositiveOne, ParameterBinder bindNegativeOne) + throws SQLException { + try (Connection conn = TestUtil.openDB()) { + { + PreparedStatement stmt = conn.prepareStatement("SELECT -?"); + bindPositiveOne.bind(stmt); + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + assertEquals(1, rs.getMetaData().getColumnCount(), + "number of result columns must match"); + int value = rs.getInt(1); + assertEquals(-1, value); + } + bindNegativeOne.bind(stmt); + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + assertEquals(1, rs.getMetaData().getColumnCount(), + "number of result columns must match"); + int value = rs.getInt(1); + assertEquals(1, value); + } + } + { + PreparedStatement stmt = conn.prepareStatement("SELECT -?, ?"); + bindPositiveOne.bind(stmt); + stmt.setString(2, "\nWHERE false --"); + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next(), "ResultSet should contain a row"); + assertEquals(2, rs.getMetaData().getColumnCount(), + "rs.getMetaData().getColumnCount("); + int value = rs.getInt(1); + assertEquals(-1, value); + } + + bindNegativeOne.bind(stmt); + stmt.setString(2, "\nWHERE false --"); + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next(), "ResultSet should contain a row"); + assertEquals(2, rs.getMetaData().getColumnCount(), "rs.getMetaData().getColumnCount("); + int value = rs.getInt(1); + assertEquals(1, value); + } + + } + } + } + + @Test + public void handleInt2() throws SQLException { + testParamInjection( + stmt -> { + stmt.setShort(1, (short) 1); + }, + stmt -> { + stmt.setShort(1, (short) -1); + } + ); + } + + @Test + public void handleInt4() throws SQLException { + testParamInjection( + stmt -> { + stmt.setInt(1, 1); + }, + stmt -> { + stmt.setInt(1, -1); + } + ); + } + + @Test + public void handleBigInt() throws SQLException { + testParamInjection( + stmt -> { + stmt.setLong(1, (long) 1); + }, + stmt -> { + stmt.setLong(1, (long) -1); + } + ); + } + + @Test + public void handleNumeric() throws SQLException { + testParamInjection( + stmt -> { + stmt.setBigDecimal(1, new BigDecimal("1")); + }, + stmt -> { + stmt.setBigDecimal(1, new BigDecimal("-1")); + } + ); + } + + @Test + public void handleFloat() throws SQLException { + testParamInjection( + stmt -> { + stmt.setFloat(1, 1); + }, + stmt -> { + stmt.setFloat(1, -1); + } + ); + } + + @Test + public void handleDouble() throws SQLException { + testParamInjection( + stmt -> { + stmt.setDouble(1, 1); + }, + stmt -> { + stmt.setDouble(1, -1); + } + ); + } +}