CVE-2022-21724 (Java, published 2022-02-02T12:15Z)
Description: value: 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 v3 base score: 9.8)
Weakness: CWE-665 \u2014 Improper Initialization
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.
Repository: https://github.com/pgjdbc/pgjdbc
Files changed:
pgjdbc/src/main/java/org/postgresql/core/SocketFactoryFactory.java
pgjdbc/src/main/java/org/postgresql/core/v3/AuthenticationPluginManager.java
pgjdbc/src/main/java/org/postgresql/ssl/LibPQFactory.java
pgjdbc/src/main/java/org/postgresql/ssl/MakeSSL.java
pgjdbc/src/main/java/org/postgresql/util/ObjectFactory.java
pgjdbc/src/test/java/org/postgresql/test/util/ObjectFactoryTest.java
Commit message: Merge pull request from GHSA-v7wg-cpwc-24m4

This ensures arbitrary classes can't be passed instead of
AuthenticationPlugin, SocketFactory, SSLSocketFactory, CallbackHandler, HostnameVerifier

Vulnerable code:
      return (SocketFactory) ObjectFactory.instantiate(socketFactoryClassName, info, true,
      return (SSLSocketFactory) ObjectFactory.instantiate(classname, info, true,
        authPlugin = (AuthenticationPlugin) ObjectFactory.instantiate(authPluginClassName, info,
        LOGGER.log(Level.FINE, "Unable to load Authentication Plugin " + ex.toString());
        throw new PSQLException(ex.getMessage(), PSQLState.UNEXPECTED_ERROR);
            GT.tr("The server requested password-based authentication, but no password was provided."),
        cbh = (CallbackHandler) ObjectFactory.instantiate(sslpasswordcallback, info, false, null);
        hvn = (HostnameVerifier) instantiate(sslhostnameverifier, info, false, null);
  public static Object instantiate(String classname, Properties info, boolean tryString,
    Constructor<?> ctor = null;
    Class<?> cls = Class.forName(classname);

Fixed code:
      return ObjectFactory.instantiate(SocketFactory.class, socketFactoryClassName, info, true,
      return ObjectFactory.instantiate(SSLSocketFactory.class, classname, info, true,
        authPlugin = ObjectFactory.instantiate(AuthenticationPlugin.class, authPluginClassName, info,
        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);
            GT.tr("The server requested password-based authentication, but no password was provided by plugin {0}",
                PGProperty.AUTHENTICATION_PLUGIN_CLASS_NAME.get(info)),
        cbh = ObjectFactory.instantiate(CallbackHandler.class, sslpasswordcallback, info, false, null);
        hvn = instantiate(HostnameVerifier.class, sslhostnameverifier, info, false, null);
  public static <T> T instantiate(Class<T> expectedClass, String classname, Properties info,
      boolean tryString,
    Constructor<? extends T> ctor = null;
    Class<? extends T> cls = Class.forName(classname).asSubclass(expectedClass);
/*
 * 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);
    });
  }
}