CVE-2022-21724 (published 2022-02-02T12:15Z)

pgjdbc is the offical PostgreSQL JDBC Driver. A security hole was found in the jdbc driver for postgresql database while doing security research. The system using the postgresql library will be attacked when attacker control the jdbc url or properties. pgjdbc instantiates plugin instances based on class names provided via `authenticationPluginClassName`, `sslhostnameverifier`, `socketFactory`, `sslfactory`, `sslpasswordcallback` connection properties. However, the driver did not verify if the class implements the expected interface before instantiating the class. This can lead to code execution loaded via arbitrary classes. Users using plugins are advised to upgrade. There are no known workarounds for this issue.

Severity: HIGH
CVSS v2 base score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-665
cwe_name: Improper Initialization
description: The product does not initialize or incorrectly initializes a resource, which might leave the resource in an unexpected state when it is accessed or used.
extended_description: This can have security implications when the associated resource is expected to have certain properties or values, such as a variable that determines whether a user has been authenticated or not.
         

Fix information:
repo_url: https://github.com/pgjdbc/pgjdbc
commit_details: repo_url: https://github.com/pgjdbc/pgjdbc
author: Vladimir Sitnikov
author_date: 2022-02-01 15:27:13+03:00
msg: Merge pull request from GHSA-v7wg-cpwc-24m4

This ensures arbitrary classes can't be passed instead of
AuthenticationPlugin, SocketFactory, SSLSocketFactory, CallbackHandler, HostnameVerifier
repository: repo_url: https://github.com/pgjdbc/pgjdbc
repo_name: pgjdbc/pgjdbc
description: Postgresql JDBC Driver
repo_language: Java
owner: pgjdbc
file_changes: filename: SocketFactoryFactory.java
change_type: ModificationType.MODIFY
diff: @@ -36,7 +36,7 @@ public class SocketFactoryFactory {
       return SocketFactory.getDefault();
     }
     try {
-      return (SocketFactory) ObjectFactory.instantiate(socketFactoryClassName, info, true,
+      return ObjectFactory.instantiate(SocketFactory.class, socketFactoryClassName, info, true,
           PGProperty.SOCKET_FACTORY_ARG.get(info));
     } catch (Exception e) {
       throw new PSQLException(
@@ -61,7 +61,7 @@ public class SocketFactoryFactory {
       return new LibPQFactory(info);
     }
     try {
-      return (SSLSocketFactory) ObjectFactory.instantiate(classname, info, true,
+      return ObjectFactory.instantiate(SSLSocketFactory.class, classname, info, true,
           PGProperty.SSL_FACTORY_ARG.get(info));
     } catch (Exception e) {
       throw new PSQLException(

programming_language: Java
filename: AuthenticationPluginManager.java
change_type: ModificationType.MODIFY
diff: @@ -66,11 +66,12 @@ class AuthenticationPluginManager {
     } else {
       AuthenticationPlugin authPlugin;
       try {
-        authPlugin = (AuthenticationPlugin) ObjectFactory.instantiate(authPluginClassName, info,
+        authPlugin = ObjectFactory.instantiate(AuthenticationPlugin.class, authPluginClassName, info,
             false, null);
       } catch (Exception ex) {
-        LOGGER.log(Level.FINE, "Unable to load Authentication Plugin " + ex.toString());
-        throw new PSQLException(ex.getMessage(), PSQLState.UNEXPECTED_ERROR);
+        String msg = GT.tr("Unable to load Authentication Plugin {0}", authPluginClassName);
+        LOGGER.log(Level.FINE, msg, ex);
+        throw new PSQLException(msg, PSQLState.INVALID_PARAMETER_VALUE, ex);
       }
 
       password = authPlugin.getPassword(type);
@@ -106,7 +107,8 @@ class AuthenticationPluginManager {
     byte[] encodedPassword = withPassword(type, info, password -> {
       if (password == null) {
         throw new PSQLException(
-            GT.tr("The server requested password-based authentication, but no password was provided."),
+            GT.tr("The server requested password-based authentication, but no password was provided by plugin {0}",
+                PGProperty.AUTHENTICATION_PLUGIN_CLASS_NAME.get(info)),
             PSQLState.CONNECTION_REJECTED);
       }
       ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(password));

programming_language: Java
filename: LibPQFactory.java
change_type: ModificationType.MODIFY
diff: @@ -56,7 +56,7 @@ public class LibPQFactory extends WrappedFactory {
     String sslpasswordcallback = PGProperty.SSL_PASSWORD_CALLBACK.get(info);
     if (sslpasswordcallback != null) {
       try {
-        cbh = (CallbackHandler) ObjectFactory.instantiate(sslpasswordcallback, info, false, null);
+        cbh = ObjectFactory.instantiate(CallbackHandler.class, sslpasswordcallback, info, false, null);
       } catch (Exception e) {
         throw new PSQLException(
           GT.tr("The password callback class provided {0} could not be instantiated.",

programming_language: Java
filename: MakeSSL.java
change_type: ModificationType.MODIFY
diff: @@ -64,7 +64,7 @@ public class MakeSSL extends ObjectFactory {
       sslhostnameverifier = "PgjdbcHostnameVerifier";
     } else {
       try {
-        hvn = (HostnameVerifier) instantiate(sslhostnameverifier, info, false, null);
+        hvn = instantiate(HostnameVerifier.class, sslhostnameverifier, info, false, null);
       } catch (Exception e) {
         throw new PSQLException(
             GT.tr("The HostnameVerifier class provided {0} could not be instantiated.",

programming_language: Java
filename: ObjectFactory.java
change_type: ModificationType.MODIFY
diff: @@ -36,14 +36,15 @@ public class ObjectFactory {
    * @throws IllegalAccessException if something goes wrong
    * @throws InvocationTargetException if something goes wrong
    */
-  public static Object instantiate(String classname, Properties info, boolean tryString,
+  public static <T> T instantiate(Class<T> expectedClass, String classname, Properties info,
+      boolean tryString,
       @Nullable String stringarg)
       throws ClassNotFoundException, SecurityException, NoSuchMethodException,
           IllegalArgumentException, InstantiationException, IllegalAccessException,
           InvocationTargetException {
     @Nullable Object[] args = {info};
-    Constructor<?> ctor = null;
-    Class<?> cls = Class.forName(classname);
+    Constructor<? extends T> ctor = null;
+    Class<? extends T> cls = Class.forName(classname).asSubclass(expectedClass);
     try {
       ctor = cls.getConstructor(Properties.class);
     } catch (NoSuchMethodException ignored) {

programming_language: Java
filename: ObjectFactoryTest.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2022, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.util;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.postgresql.PGProperty;
+import org.postgresql.jdbc.SslMode;
+import org.postgresql.test.TestUtil;
+import org.postgresql.util.ObjectFactory;
+import org.postgresql.util.PSQLState;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Assertions;
+import org.opentest4j.MultipleFailuresError;
+
+import java.sql.SQLException;
+import java.util.Properties;
+
+import javax.net.SocketFactory;
+
+public class ObjectFactoryTest {
+  Properties props = new Properties();
+
+  static class BadObject {
+    static boolean wasInstantiated = false;
+
+    BadObject() {
+      wasInstantiated = true;
+      throw new RuntimeException("I should not be instantiated");
+    }
+  }
+
+  private void testInvalidInstantiation(PGProperty prop, PSQLState expectedSqlState) {
+    prop.set(props, BadObject.class.getName());
+
+    BadObject.wasInstantiated = false;
+    SQLException ex = assertThrows(SQLException.class, () -> {
+      TestUtil.openDB(props);
+    });
+
+    try {
+      Assertions.assertAll(
+          () -> assertFalse(BadObject.wasInstantiated, "ObjectFactory should not have "
+              + "instantiated bad object for " + prop),
+          () -> assertEquals(expectedSqlState.getState(), ex.getSQLState(), () -> "#getSQLState()"),
+          () -> {
+            assertThrows(
+                ClassCastException.class,
+                () -> {
+                  throw ex.getCause();
+                },
+                () -> "Wrong class specified for " + prop.name()
+                    + " => ClassCastException is expected in SQLException#getCause()"
+            );
+          }
+      );
+    } catch (MultipleFailuresError e) {
+      // Add the original exception so it is easier to understand the reason for the test to fail
+      e.addSuppressed(ex);
+      throw e;
+    }
+  }
+
+  @Test
+  public void testInvalidSocketFactory() {
+    testInvalidInstantiation(PGProperty.SOCKET_FACTORY, PSQLState.CONNECTION_FAILURE);
+  }
+
+  @Test
+  public void testInvalidSSLFactory() {
+    TestUtil.assumeSslTestsEnabled();
+    // We need at least "require" to trigger SslSockerFactory instantiation
+    PGProperty.SSL_MODE.set(props, SslMode.REQUIRE.value);
+    testInvalidInstantiation(PGProperty.SSL_FACTORY, PSQLState.CONNECTION_FAILURE);
+  }
+
+  @Test
+  public void testInvalidAuthenticationPlugin() {
+    testInvalidInstantiation(PGProperty.AUTHENTICATION_PLUGIN_CLASS_NAME,
+        PSQLState.INVALID_PARAMETER_VALUE);
+  }
+
+  @Test
+  public void testInvalidSslHostnameVerifier() {
+    TestUtil.assumeSslTestsEnabled();
+    // Hostname verification is done at verify-full level only
+    PGProperty.SSL_MODE.set(props, SslMode.VERIFY_FULL.value);
+    PGProperty.SSL_ROOT_CERT.set(props, TestUtil.getSslTestCertPath("goodroot.crt"));
+    testInvalidInstantiation(PGProperty.SSL_HOSTNAME_VERIFIER, PSQLState.CONNECTION_FAILURE);
+  }
+
+  @Test
+  public void testInstantiateInvalidSocketFactory() {
+    Properties props = new Properties();
+    assertThrows(ClassCastException.class, () -> {
+      ObjectFactory.instantiate(SocketFactory.class, BadObject.class.getName(), props,
+          false, null);
+    });
+  }
+}

programming_language: Java