Commit
This ensures arbitrary classes can't be passed instead of AuthenticationPlugin, SocketFactory, SSLSocketFactory, CallbackHandler, HostnameVerifier
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
davecramer
via email
Member
|
||
try { | ||
ctor = cls.getConstructor(Properties.class); | ||
} catch (NoSuchMethodException ignored) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
} | ||
} |
Correct me if I'm wrong but this
Class.forName
will still initialize the class, which means executing thestatic
initializers, andstatic
fields' initializers (https://docs.oracle.com/javase/specs/jls/se11/html/jls-12.html#jls-12.4), so in effect you've only guarded the execution of the constructor, but not of all possible code execution paths.To fully protect against (some) RCE, you would have to initialize the class after you've ensured it's of a correct type, i.e. load the class with
forName(classname, false, ObjectFactory.class.getClassLoader())
.