File CVE-2024-1597.patch of Package postgresql-jdbc.32825

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);
+        }
+    );
+  }
+}
openSUSE Build Service is sponsored by