Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-15-SP4:Update
postgresql-jdbc
CVE-2024-1597.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File CVE-2024-1597.patch of Package postgresql-jdbc
From b9b3777671c8a5cc580e1985f61337d39d47c730 Mon Sep 17 00:00:00 2001 From: Dave Cramer <davecramer@gmail.com> Date: Mon, 19 Feb 2024 08:20:59 -0500 Subject: [PATCH] Merge pull request from GHSA-24rp-q3w6-vc56 * SQL Injection via line comment generation for 42_2_x * fix: Add parentheses around NULL parameter values in simple query mode * simplify code, handle binary and add tests * remove extra spaces --------- Co-authored-by: Sehrope Sarkuni <sehrope@jackdb.com> --- .../core/v3/SimpleParameterList.java | 118 ++++++++++++------ .../core/v3/V3ParameterListTests.java | 6 +- .../jdbc/ParameterInjectionTest.java | 67 ++++++++++ 3 files changed, 149 insertions(+), 42 deletions(-) create mode 100644 pgjdbc/src/test/java/org/postgresql/jdbc/ParameterInjectionTest.java Index: postgresql-42.2.25-jdbc-src/src/main/java/org/postgresql/core/v3/SimpleParameterList.java =================================================================== --- postgresql-42.2.25-jdbc-src.orig/src/main/java/org/postgresql/core/v3/SimpleParameterList.java +++ postgresql-42.2.25-jdbc-src/src/main/java/org/postgresql/core/v3/SimpleParameterList.java @@ -172,6 +172,58 @@ class SimpleParameterList implements V3P bind(index, NULL_OBJECT, oid, binaryTransfer); } + /** + * <p>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.</p> + * + * <p>For example, "123" and "int4" becomes "('123'::int)"</p> + * + * <p>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.</p> + * + * <p>For example if our input SQL is: <code>SELECT ?b</code></p> + * + * <p>Using a parameter value of '{}' and type of json we'd get:</p> + * + * <pre> + * test=# SELECT ('{}'::json)b; + * b + * ---- + * {} + * </pre> + * + * <p>But without the parentheses the result changes:</p> + * + * <pre> + * test=# SELECT '{}'::jsonb; + * jsonb + * ------- + * {} + * </pre> + **/ + 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; @@ -179,93 +231,104 @@ class SimpleParameterList implements V3P 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)"; + } + 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 Index: postgresql-42.2.25-jdbc-src/src/test/java/org/postgresql/core/v3/V3ParameterListTests.java =================================================================== --- postgresql-42.2.25-jdbc-src.orig/src/test/java/org/postgresql/core/v3/V3ParameterListTests.java +++ postgresql-42.2.25-jdbc-src/src/test/java/org/postgresql/core/v3/V3ParameterListTests.java @@ -58,8 +58,8 @@ public class V3ParameterListTests { 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()); + } } Index: postgresql-42.2.25-jdbc-src/src/test/java/org/postgresql/jdbc/ParameterInjectionTest.java =================================================================== --- /dev/null +++ postgresql-42.2.25-jdbc-src/src/test/java/org/postgresql/jdbc/ParameterInjectionTest.java @@ -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); + } + ); + } +}
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor