Skip to content

Commit 466a0f9

Browse files
authored
Merge commit from fork
Perform the IJackcessOpenerInterface type check before calling getConstructor().newInstance() when resolving a custom jackcessOpener class. Previously the class was instantiated first and checked afterwards, so the no-arg constructor of any named class would run even if it didn't implement the required interface. Also document that jackcessOpener must only be set from trusted configuration, and add regression tests. Refs: GHSA-5898-r52v-fprf Reported-by: Fushuling (https://github.com/Fushuling)
1 parent ef02369 commit 466a0f9

4 files changed

Lines changed: 118 additions & 7 deletions

File tree

src/main/java/net/ucanaccess/converters/Metadata.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ public enum Property {
4848
ignoreCase(Boolean.class, true, 10),
4949
immediatelyReleaseResources(Boolean.class, false, 10),
5050
inactivityTimeout(Integer.class, 2, 10),
51+
/**
52+
* Fully qualified class name of a custom {@code IJackcessOpenerInterface} implementation
53+
* to use for opening/decrypting the database file.
54+
* <p>
55+
* <strong>Security note:</strong> the value of this property is instantiated via reflection
56+
* using its public no-arg constructor. It must therefore only ever be set from trusted,
57+
* static application configuration. Never populate this property from untrusted or
58+
* user-supplied input (e.g. HTTP request parameters, values forwarded unchecked from an
59+
* external system), as doing so allows an attacker to trigger the construction of an
60+
* arbitrary class implementing {@code IJackcessOpenerInterface} that is reachable on the
61+
* application's classpath.
62+
*/
5163
jackcessOpener(String.class, null, 500),
5264
keepMirror(String.class, null, 500),
5365
lobScale(Integer.class, 2, 2, 1, 2, 4, 8, 16, 32),

src/main/java/net/ucanaccess/jdbc/UcanaccessDriver.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,13 +280,31 @@ public boolean jdbcCompliant() {
280280
return true;
281281
}
282282

283+
/**
284+
* Loads the class with the given name and instantiates it via its public no-arg constructor,
285+
* provided it implements {@link IJackcessOpenerInterface}.
286+
* <p>
287+
* <strong>Security note:</strong> {@code className} must only ever originate from trusted,
288+
* static application configuration (see the {@code jackcessOpener} connection property).
289+
* It must never be built from untrusted or user-supplied input, since this method executes
290+
* the no-arg constructor of whatever class is named, and any class implementing
291+
* {@link IJackcessOpenerInterface} that is reachable on the classpath will pass the type
292+
* check regardless of what its constructor does.
293+
*
294+
* @param className fully qualified name of a class implementing {@link IJackcessOpenerInterface}
295+
* @return a new instance of the given class
296+
* @throws UcanaccessSQLException if the class cannot be loaded, does not implement
297+
* {@link IJackcessOpenerInterface}, or cannot be instantiated via its no-arg constructor
298+
*/
283299
private IJackcessOpenerInterface newJackcessOpenerInstance(String className) throws UcanaccessSQLException {
284-
Object instance = Try.catching(() -> Class.forName(className).getConstructor().newInstance()).orThrow(ex -> new UcanaccessSQLException("Failed to instantiate " + className, ex));
285-
286-
if (instance instanceof IJackcessOpenerInterface) {
287-
return (IJackcessOpenerInterface) instance;
300+
Class<?> clazz = Try.catching(() -> Class.forName(className))
301+
.orThrow(ex -> new UcanaccessSQLException("Failed to load class " + className, ex));
302+
if (!IJackcessOpenerInterface.class.isAssignableFrom(clazz)) {
303+
throw new UcanaccessSQLException("Jackess Opener class must implement " + IJackcessOpenerInterface.class.getName());
288304
}
289-
throw new UcanaccessSQLException("Jackess Opener class must implement " + IJackcessOpenerInterface.class.getName());
305+
Object instance = Try.catching(() -> clazz.getConstructor().newInstance())
306+
.orThrow(ex -> new UcanaccessSQLException("Failed to instantiate " + className, ex));
307+
return (IJackcessOpenerInterface) instance;
290308
}
291309

292310
/**

src/site/xhtml/20-getting-started.xhtml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
&lt;dependency&gt;
2222
&lt;groupId&gt;io.github.spannm&lt;/groupId&gt;
2323
&lt;artifactId&gt;ucanaccess&lt;/artifactId&gt;
24-
&lt;version&gt;5.1.3&lt;/version&gt;
24+
&lt;version&gt;5.1.7&lt;/version&gt;
2525
&lt;/dependency&gt;</pre>
2626

2727
<h2>Establishing a UCanAccess connection</h2>
@@ -178,6 +178,15 @@ Connection connExample = DriverManager.getConnection(&quot;jdbc:ucanaccess://c:/
178178
for UCanAccess2. Notice that you must use UCanAccess 2.x.x with jackcess-encrypt-2.x.x and all related
179179
dependencies</strong><br/>
180180
</p>
181+
<p>
182+
<strong>Security note:</strong> the class name passed to <code>jackcessOpener</code> is loaded and
183+
instantiated via reflection using its public no-arg constructor. This parameter must therefore only ever
184+
be set from trusted, static application configuration &#8211; never from untrusted or user-supplied input
185+
(e.g. values taken from an HTTP request, or forwarded unchecked from an external system). Populating
186+
<code>jackcessOpener</code> from untrusted input could allow an attacker to trigger the construction of
187+
an arbitrary class implementing <i>IJackcessOpenerInterface</i> that is reachable on the application's
188+
classpath.
189+
</p>
181190
<pre>
182191
package yourPackage.example;
183192

src/test/java/net/ucanaccess/jdbc/UcanaccessDriverTest.java

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
package net.ucanaccess.jdbc;
22

3-
import static net.ucanaccess.converters.Metadata.Property.*;
3+
import static net.ucanaccess.converters.Metadata.Property.columnOrder;
4+
import static net.ucanaccess.converters.Metadata.Property.concatNulls;
5+
import static net.ucanaccess.converters.Metadata.Property.encrypt;
46
import static org.assertj.core.api.Assertions.assertThat;
7+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
58

9+
import io.github.spannm.jackcess.Database;
610
import net.ucanaccess.converters.Metadata.Property;
11+
import net.ucanaccess.exception.UcanaccessSQLException;
712
import net.ucanaccess.test.UcanaccessBaseTest;
813
import org.junit.jupiter.api.Test;
914

15+
import java.io.File;
16+
import java.lang.reflect.InvocationTargetException;
17+
import java.lang.reflect.Method;
1018
import java.util.LinkedHashMap;
1119
import java.util.Map;
1220
import java.util.Properties;
@@ -42,4 +50,68 @@ void testVersion() {
4250
assertThat(driver.getMinorVersion()).isGreaterThanOrEqualTo(1);
4351
}
4452

53+
/**
54+
* A class name passed via the {@code jackcessOpener}
55+
* property must be rejected if it does not implement {@code IJackcessOpenerInterface} -
56+
* and its constructor must never execute in that case, since constructor side effects
57+
* cannot be undone once the class has been instantiated.
58+
*/
59+
@Test
60+
void testNewJackcessOpenerInstance_rejectsNonImplementingClass_beforeConstruction() throws Exception {
61+
NonOpenerWithSideEffect.instantiated = false;
62+
63+
assertThatThrownBy(() -> invokeNewJackcessOpenerInstance(NonOpenerWithSideEffect.class.getName()))
64+
.isInstanceOf(UcanaccessSQLException.class)
65+
.hasMessageContaining("must implement");
66+
67+
assertThat(NonOpenerWithSideEffect.instantiated)
68+
.as("constructor of a class not implementing IJackcessOpenerInterface must never run")
69+
.isFalse();
70+
}
71+
72+
@Test
73+
void testNewJackcessOpenerInstance_acceptsImplementingClass() throws Exception {
74+
Object instance = invokeNewJackcessOpenerInstance(ValidTestOpener.class.getName());
75+
76+
assertThat(instance).isInstanceOf(IJackcessOpenerInterface.class);
77+
}
78+
79+
@Test
80+
void testNewJackcessOpenerInstance_wrapsUnknownClassNameInSqlException() {
81+
assertThatThrownBy(() -> invokeNewJackcessOpenerInstance("does.not.Exist"))
82+
.isInstanceOf(UcanaccessSQLException.class);
83+
}
84+
85+
private Object invokeNewJackcessOpenerInstance(String className) throws Exception {
86+
Method method = UcanaccessDriver.class.getDeclaredMethod("newJackcessOpenerInstance", String.class);
87+
method.setAccessible(true);
88+
try {
89+
return method.invoke(new UcanaccessDriver(), className);
90+
} catch (InvocationTargetException _ex) {
91+
if (_ex.getCause() instanceof Exception) {
92+
throw (Exception) _ex.getCause();
93+
}
94+
throw _ex;
95+
}
96+
}
97+
98+
/** Simulates a malicious class unrelated to IJackcessOpenerInterface with a dangerous constructor side effect. */
99+
static final class NonOpenerWithSideEffect {
100+
static boolean instantiated = false;
101+
102+
public NonOpenerWithSideEffect() {
103+
instantiated = true;
104+
}
105+
}
106+
107+
static final class ValidTestOpener implements IJackcessOpenerInterface {
108+
public ValidTestOpener() {
109+
}
110+
111+
@Override
112+
public Database open(File _file, String _password) {
113+
return null;
114+
}
115+
}
116+
45117
}

0 commit comments

Comments
 (0)