CVE-2024-32888: Insufficient Information (NVD-CWE-noinfo)
value: The Amazon JDBC Driver for Redshift is a Type 4 JDBC driver that provides database connectivity through the standard JDBC application program interfaces (APIs) available in the Java Platform, Enterprise Editions. Prior to version 2.1.0.28, SQL injection is possible when using the non-default connection property `preferQueryMode=simple` in combination with application code which has a vulnerable SQL that negates a parameter value. There is no vulnerability in the driver when using the default, extended query mode. Note that `preferQueryMode` is not a supported parameter in Redshift JDBC driver, and is inherited code from Postgres JDBC driver. Users who do not override default settings to utilize this unsupported query mode are not affected. This issue is patched in driver version 2.1.0.28. As a workaround, do not use the connection property `preferQueryMode=simple`. (NOTE: Those who do not explicitly specify a query mode use the default of extended query mode and are not affected by this issue.)
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-15T03:15Z
Repository: https://github.com/aws/amazon-redshift-jdbc-driver
Language: Java
Files changed: src/main/java/com/amazon/redshift/core/v3/SimpleParameterList.java
Commit date: 2024-05-14 16:15:56 -0700
Commit message: Security improvements.

Diff: diff --git a/src/main/java/com/amazon/redshift/core/v3/SimpleParameterList.java b/src/main/java/com/amazon/redshift/core/v3/SimpleParameterList.java
index 300cb92..932f408 100644
--- a/src/main/java/com/amazon/redshift/core/v3/SimpleParameterList.java
+++ b/src/main/java/com/amazon/redshift/core/v3/SimpleParameterList.java
@@ -182,107 +182,134 @@ class SimpleParameterList implements V3ParameterList {
       binaryTransfer = BINARY;
     }
     bind(index, NULL_OBJECT, oid, binaryTransfer);
   }
 
+  private static String quoteAndCast(String text, 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(int index, boolean standardConformingStrings) {
     --index;
     if (paramValues[index] == null) {
       return "?";
     } else if (paramValues[index] == NULL_OBJECT) {
       return "NULL";
-    } else if ((flags[index] & BINARY) == BINARY) {
+    }
+    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[]) paramValues[index], 0);
-          return Short.toString(s);
+          textValue = Short.toString(s);
+          type = "int2";
+          break;
 
         case Oid.INT4:
           int i = ByteConverter.int4((byte[]) paramValues[index], 0);
-          return Integer.toString(i);
+          textValue = Integer.toString(i);
+          type = "int4";
+          break;
 
         case Oid.INT8:
           long l = ByteConverter.int8((byte[]) paramValues[index], 0);
-          return Long.toString(l);
+          textValue = Long.toString(l);
+          type = "int8";
+          break;
 
         case Oid.FLOAT4:
           float f = ByteConverter.float4((byte[]) paramValues[index], 0);
           if (Float.isNaN(f)) {
             return "'NaN'::real";
           }
-          return Float.toString(f);
+          textValue = Float.toString(f);
+          type = "real";
+          break;
 
         case Oid.FLOAT8:
           double d = ByteConverter.float8((byte[]) paramValues[index], 0);
           if (Double.isNaN(d)) {
             return "'NaN'::double precision";
           }
-          return Double.toString(d);
+          textValue = Double.toString(d);
+          type = "double precision";
+          break;
 
         case Oid.UUID:
-          String uuid =
+          textValue =
               new UUIDArrayAssistant().buildElement((byte[]) paramValues[index], 0, 16).toString();
-          return "'" + uuid + "'::uuid";
+          type = "uuid";
+          break;
 
         case Oid.POINT:
           RedshiftPoint pgPoint = new RedshiftPoint();
           pgPoint.setByteValue((byte[]) paramValues[index], 0);
-          return "'" + pgPoint.toString() + "'::point";
+          textValue = pgPoint.toString();
+          type = "point";
+          break;
 
         case Oid.BOX:
           RedshiftBox pgBox = new RedshiftBox();
           pgBox.setByteValue((byte[]) paramValues[index], 0);
-          return "'" + pgBox.toString() + "'::box";
+          textValue = pgBox.toString();
+          type = "box";
+          break;
+
+        default:
+          return "?";
       }
-      return "?";
     } else {
-      String param = paramValues[index].toString();
-
-      // add room for quotes + potential escaping.
-      StringBuilder p = new StringBuilder(3 + (param.length() + 10) / 10 * 11);
-
-      // 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);
-      }
-      p.append('\'');
+      textValue = paramValues[index].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.INTERVALY2M) {
-        p.append("::interval year to month");
+        type = "interval year to month";
       } else if (paramType == Oid.INTERVALD2S) {
-        p.append("::interval day to second");
+        type = "interval day to second";
       } else if (paramType == Oid.NUMERIC) {
-        p.append("::numeric");
+        type = "numeric";
+      }
+      else {
+        type = null;
       }
-      return p.toString();
     }
+    return quoteAndCast(textValue, type, standardConformingStrings);
   }
 
   @Override
   public void checkAllParametersSet() throws SQLException {
     for (int i = 0; i < paramTypes.length; ++i) {

Vulnerable code:     } else if ((flags[index] & BINARY) == BINARY) {

          return Short.toString(s);
          return Integer.toString(i);
          return Long.toString(l);
          return Float.toString(f);
          return Double.toString(d);
          String uuid =
          return "'" + uuid + "'::uuid";
          return "'" + pgPoint.toString() + "'::point";
          return "'" + pgBox.toString() + "'::box";
      return "?";
      String param = paramValues[index].toString();

      // add room for quotes + potential escaping.
      StringBuilder p = new StringBuilder(3 + (param.length() + 10) / 10 * 11);

      // 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);
      }
      p.append('\'');
        p.append("::timestamp");
        p.append("::timestamp with time zone");
        p.append("::time");
        p.append("::time with time zone");
        p.append("::date");
        p.append("::interval");
        p.append("::interval year to month");
        p.append("::interval day to second");
        p.append("::numeric");
      return p.toString();
Fixed code:   private static String quoteAndCast(String text, 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();
  }

    }
    String textValue;
    String type;
    if ((flags[index] & BINARY) == BINARY) {
          textValue = Short.toString(s);
          type = "int2";
          break;
          textValue = Integer.toString(i);
          type = "int4";
          break;
          textValue = Long.toString(l);
          type = "int8";
          break;
          textValue = Float.toString(f);
          type = "real";
          break;
          textValue = Double.toString(d);
          type = "double precision";
          break;
          textValue =
          type = "uuid";
          break;
          textValue = pgPoint.toString();
          type = "point";
          break;
          textValue = pgBox.toString();
          type = "box";
          break;

        default:
          return "?";
      textValue = paramValues[index].toString();

        type = "timestamp";
        type = "timestamp with time zone";
        type = "time";
        type = "time with time zone";
        type = "date";
        type = "interval";
        type = "interval year to month";
        type = "interval day to second";
        type = "numeric";
      }
      else {
        type = null;
    return quoteAndCast(textValue, type, standardConformingStrings);