Skip to content

Commit 1388e22

Browse files
committed
fix(link): harden UCanAccess's linked-database resolution
UCanAccess registers a custom LinkResolver in DBReference that opens any linked table path verbatim, bypassing jackcess's own DEFAULT resolver hardening for GHSA-qp68-cg9v-qh8p entirely. Since linked paths are read from the (possibly untrusted) Access database file being opened, this let a malicious database force the JVM to reach an arbitrary UNC/network path as soon as a linked table was touched. Deny automatic resolution of network/UNC linked paths by default, with an explicit opt-in via the new allowRemoteLinks connection property for applications that trust their linked network paths. An explicit reMap entry for a link is treated as trusted application configuration and bypasses the guard without probing the raw path. Local linked paths are unaffected.
1 parent 9242745 commit 1388e22

5 files changed

Lines changed: 120 additions & 3 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,23 @@ try (Connection conn = DriverManager.getConnection(url)) {
113113

114114
<p style="height: 20px;">&nbsp;</p>
115115

116+
## 🔗 Linked Tables & Untrusted Databases
117+
118+
If the `.accdb`/`.mdb` file you open contains linked tables, UCanAccess automatically opens the linked database files
119+
so those tables can be read. Local linked paths are resolved as before, but a linked table pointing to a
120+
**network/UNC path** (e.g. `\\server\share\linked.accdb`) is rejected by default with an `AccessDeniedException`
121+
such a path is taken verbatim from the database file, so a database from an untrusted source could otherwise make
122+
your JVM reach out to an arbitrary host as soon as the linked table is touched.
123+
124+
If you trust the linked network paths in a given database, opt back in explicitly via the `allowRemoteLinks`
125+
connection property:
126+
127+
```java
128+
String url = "jdbc:ucanaccess://C:/path/to/your/database.accdb;allowRemoteLinks=true";
129+
```
130+
131+
<p style="height: 20px;">&nbsp;</p>
132+
116133
## ❤️ Why this Fork?
117134

118135
The original project (developed by Marco Amadei and Gord Thompson) was the gold standard for Access connectivity but went quiet in 2020, leaving `net.sf.ucanaccess:ucanaccess` unmaintained at version 5.0.1.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ public enum Property {
4242
user(String.class, null, 500),
4343
password(String.class, null, 500),
4444

45+
/**
46+
* Whether linked tables that point to a network/UNC path (e.g. {@code \\server\share\db.accdb}) may be
47+
* resolved automatically.
48+
* <p>
49+
* <strong>Security note:</strong> the path of a linked table is taken verbatim from the Access database
50+
* file being opened. If that file is not fully trusted, an attacker-controlled link could otherwise make
51+
* the JVM open an arbitrary network/UNC path as soon as the linked table is accessed (e.g. to reach an
52+
* internal host or trigger an outbound credential-relay attempt). Automatic resolution of such paths is
53+
* therefore denied by default; enable this property only when linked network paths are known to be
54+
* trusted. Local (non-UNC) linked paths are not affected by this property.
55+
*/
56+
allowRemoteLinks(Boolean.class, false, 10),
4557
columnOrder(ColumnOrder.class, ColumnOrder.DATA, 10),
4658
concatNulls(Boolean.class, false, 10),
4759
encrypt(Boolean.class, false, 10),

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

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.github.spannm.jackcess.Table;
99
import io.github.spannm.jackcess.Table.ColumnOrder;
1010
import net.ucanaccess.converters.LoadJet;
11+
import net.ucanaccess.converters.Metadata;
1112
import net.ucanaccess.exception.UcanaccessSQLException;
1213

1314
import java.io.File;
@@ -17,6 +18,7 @@
1718
import java.lang.System.Logger.Level;
1819
import java.nio.channels.FileLock;
1920
import java.nio.charset.Charset;
21+
import java.nio.file.AccessDeniedException;
2022
import java.sql.Connection;
2123
import java.sql.DriverManager;
2224
import java.sql.ResultSet;
@@ -79,6 +81,12 @@ public class DBReference {
7981
private boolean concatNulls;
8082
private boolean mirrorRecreated;
8183
private Charset charset;
84+
/**
85+
* Whether the {@link io.github.spannm.jackcess.util.LinkResolver} registered below may automatically resolve
86+
* linked tables that point to a network/UNC path. {@code false} by default; see
87+
* {@link net.ucanaccess.converters.Metadata.Property#allowRemoteLinks}.
88+
*/
89+
private boolean allowRemoteLinks;
8290

8391
public DBReference(File fl, FileFormat ff, IJackcessOpenerInterface _jko, final String _pwd, Charset _charset)
8492
throws IOException {
@@ -102,10 +110,25 @@ public DBReference(File fl, FileFormat ff, IJackcessOpenerInterface _jko, final
102110
if (linkeeFileName == null) {
103111
throw new IOException("Cannot resolve db link");
104112
}
105-
File linkeeFile = new File(linkeeFileName);
106113
Map<String, String> emr = externalResourcesMapping;
107-
if (!linkeeFile.exists() && emr != null && emr.containsKey(linkeeFileName.toLowerCase())) {
108-
linkeeFile = new File(emr.get(linkeeFileName.toLowerCase()));
114+
String remapped = emr == null ? null : emr.get(linkeeFileName.toLowerCase());
115+
// an explicit remapping via the "reMap" connection property is trusted application configuration
116+
// and therefore bypasses the network-path guard below; the raw (untrusted) path stored in the
117+
// database file itself is not probed or opened in that case
118+
if (remapped == null && !allowRemoteLinks && isNetworkPath(linkeeFileName)) {
119+
throw new AccessDeniedException(linkeeFileName, null,
120+
"Linked database points to a network/UNC path; automatic resolution of such paths is "
121+
+ "disabled by default because the linked path is taken verbatim from the (possibly "
122+
+ "untrusted) Access database file. Set the '" + Metadata.Property.allowRemoteLinks
123+
+ "' connection property to true only if linked network paths are trusted.");
124+
}
125+
// for a network path with an explicit remapping, use the remapped (trusted) path directly without
126+
// probing the raw network path first
127+
File linkeeFile = remapped != null && isNetworkPath(linkeeFileName)
128+
? new File(remapped)
129+
: new File(linkeeFileName);
130+
if (!linkeeFile.exists() && remapped != null) {
131+
linkeeFile = new File(remapped);
109132
}
110133
if (!linkeeFile.exists()) {
111134
logger.log(Level.WARNING, "External file {0} does not exist", linkeeFile.getAbsolutePath());
@@ -121,6 +144,21 @@ public DBReference(File fl, FileFormat ff, IJackcessOpenerInterface _jko, final
121144
}
122145
}
123146

147+
/**
148+
* Determines whether the given linked database path is a network/UNC path, e.g. {@code \\server\share\db.accdb},
149+
* {@code //server/share/db.accdb} or a device-style UNC path such as {@code \\?\UNC\server\share\db.accdb}.
150+
* <p>
151+
* Such paths are excluded from automatic resolution by default since they let an untrusted Access database
152+
* file trigger an outbound network connection (see {@link #allowRemoteLinks}).
153+
*
154+
* @param _fileName the linked database path as stored in the Access database file
155+
* @return {@code true} if the path is a network/UNC path, {@code false} otherwise
156+
*/
157+
static boolean isNetworkPath(String _fileName) {
158+
String normalized = _fileName.replace('/', '\\');
159+
return normalized.startsWith("\\\\");
160+
}
161+
124162
public Database open(File _dbfl, String _pwd) throws IOException {
125163
Database ret = jko.open(_dbfl, _pwd, charset);
126164
if (columnOrderDisplay) {
@@ -572,6 +610,18 @@ public void setExternalResourcesMapping(Map<String, String> _externalResourcesMa
572610
externalResourcesMapping = _externalResourcesMapping;
573611
}
574612

613+
/**
614+
* Enables or disables automatic resolution of linked tables that point to a network/UNC path.
615+
* <p>
616+
* Disabled by default; see the security note on {@link net.ucanaccess.converters.Metadata.Property#allowRemoteLinks}
617+
* for why this matters when opening database files from an untrusted source.
618+
*
619+
* @param _allowRemoteLinks {@code true} to allow automatic resolution of network/UNC linked paths
620+
*/
621+
public void setAllowRemoteLinks(boolean _allowRemoteLinks) {
622+
allowRemoteLinks = _allowRemoteLinks;
623+
}
624+
575625
public File getToKeepHsql() {
576626
return toKeepHsql;
577627
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ public Connection connect(String _url, Properties _props) throws SQLException {
149149
if (props.containsKey(preventReloading)) {
150150
dbRef.setPreventReloading(Boolean.parseBoolean(props.get(preventReloading)));
151151
}
152+
if (props.containsKey(allowRemoteLinks)) {
153+
dbRef.setAllowRemoteLinks(Boolean.parseBoolean(props.get(allowRemoteLinks)));
154+
}
152155
if (props.containsKey(reMap)) {
153156
Map<String, String> map = Arrays.stream(props.get(reMap).split("&")).map(s -> s.split("\\|")).filter(arr -> arr.length == 2)
154157
.collect(Collectors.toMap(k1 -> k1[0], v1 -> v1[1], (v1, v2) -> v1, LinkedHashMap::new));
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package net.ucanaccess.jdbc;
2+
3+
import static org.junit.jupiter.api.Assertions.assertFalse;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import org.junit.jupiter.params.ParameterizedTest;
7+
import org.junit.jupiter.params.provider.ValueSource;
8+
9+
class DBReferenceTest {
10+
11+
@ParameterizedTest
12+
@ValueSource(strings = {
13+
"\\\\server\\share\\linked.accdb",
14+
"//server/share/linked.accdb",
15+
"\\\\?\\UNC\\server\\share\\linked.accdb",
16+
"\\\\.\\linked.accdb",
17+
"\\/server/share/linked.accdb"
18+
})
19+
void testIsNetworkPathDetectsUncAndNetworkPaths(String _linkedDbName) {
20+
assertTrue(DBReference.isNetworkPath(_linkedDbName));
21+
}
22+
23+
@ParameterizedTest
24+
@ValueSource(strings = {
25+
"linked.accdb",
26+
"../outside.accdb",
27+
"..\\outside.accdb",
28+
"c:\\db\\linked.accdb",
29+
"/home/user/db/linked.accdb"
30+
})
31+
void testIsNetworkPathAllowsLocalPaths(String _linkedDbName) {
32+
assertFalse(DBReference.isNetworkPath(_linkedDbName));
33+
}
34+
35+
}

0 commit comments

Comments
 (0)