Skip to content

Commit e8a3a4b

Browse files
github-actions[bot]justinmcleanyuqi1129
authored
[#7547] Validate JDBC (#7611)
### What changes were proposed in this pull request? Validate JDBC connection for unsafe operations. ### Why are the changes needed? To stop potentially unsafe operations. Fix: #7547 ### Does this PR introduce _any_ user-facing change? No ### How was this patch tested? Locally, plus added unit tests. Co-authored-by: Justin Mclean <[email protected]> Co-authored-by: Mini Yu <[email protected]>
1 parent f35fdb0 commit e8a3a4b

File tree

2 files changed

+121
-0
lines changed

2 files changed

+121
-0
lines changed

catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/utils/DataSourceUtils.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
package org.apache.gravitino.catalog.jdbc.utils;
2020

2121
import java.sql.SQLException;
22+
import java.util.Arrays;
23+
import java.util.List;
2224
import java.util.Map;
2325
import java.util.Properties;
2426
import javax.sql.DataSource;
@@ -37,6 +39,26 @@ public class DataSourceUtils {
3739
/** SQL statements for database connection pool testing. */
3840
private static final String POOL_TEST_QUERY = "SELECT 1";
3941

42+
private static final List<String> unsafeMySQLParameters =
43+
Arrays.asList(
44+
"maxAllowedPacket",
45+
"autoDeserialize",
46+
"queryInterceptors",
47+
"statementInterceptors",
48+
"detectCustomCollations",
49+
"allowloadlocalinfile",
50+
"allowUrlInLocalInfile",
51+
"allowLoadLocalInfileInPath");
52+
53+
private static final List<String> unsafePostgresParameters =
54+
Arrays.asList(
55+
"socketFactory",
56+
"socketFactoryArg",
57+
"sslfactory",
58+
"sslhostnameverifier",
59+
"sslpasswordcallback",
60+
"authenticationPluginClassName");
61+
4062
public static DataSource createDataSource(Map<String, String> properties) {
4163
return createDataSource(new JdbcConfig(properties));
4264
}
@@ -51,6 +73,8 @@ public static DataSource createDataSource(JdbcConfig jdbcConfig)
5173
}
5274

5375
private static DataSource createDBCPDataSource(JdbcConfig jdbcConfig) throws Exception {
76+
validateJdbcConfig(
77+
jdbcConfig.getJdbcDriver(), jdbcConfig.getJdbcUrl(), jdbcConfig.getAllConfig());
5478
BasicDataSource basicDataSource =
5579
BasicDataSourceFactory.createDataSource(getProperties(jdbcConfig));
5680
String jdbcUrl = jdbcConfig.getJdbcUrl();
@@ -76,6 +100,61 @@ private static Properties getProperties(JdbcConfig jdbcConfig) {
76100
return properties;
77101
}
78102

103+
private static String recursiveDecode(String url) {
104+
String prev;
105+
String decoded = url;
106+
int max = 5;
107+
108+
do {
109+
prev = decoded;
110+
try {
111+
decoded = java.net.URLDecoder.decode(prev, "UTF-8");
112+
} catch (Exception e) {
113+
throw new GravitinoRuntimeException("Unable to decode JDBC URL");
114+
}
115+
} while (!prev.equals(decoded) && --max > 0);
116+
117+
return decoded;
118+
}
119+
120+
private static void checkUnsafeParameters(
121+
String url, Map<String, String> config, List<String> unsafeParams, String dbType) {
122+
123+
String lowerUrl = url.toLowerCase();
124+
125+
for (String param : unsafeParams) {
126+
String lowerParam = param.toLowerCase();
127+
if (lowerUrl.contains(lowerParam) || containsValueIgnoreCase(config, param)) {
128+
throw new GravitinoRuntimeException(
129+
"Unsafe %s parameter '%s' detected in JDBC URL", dbType, param);
130+
}
131+
}
132+
}
133+
134+
public static void validateJdbcConfig(String driver, String url, Map<String, String> all) {
135+
String lowerUrl = url.toLowerCase();
136+
String decodedUrl = recursiveDecode(lowerUrl);
137+
138+
if (driver != null) {
139+
if (decodedUrl.startsWith("jdbc:mysql")) {
140+
checkUnsafeParameters(decodedUrl, all, unsafeMySQLParameters, "MySQL");
141+
} else if (decodedUrl.startsWith("jdbc:mariadb")) {
142+
checkUnsafeParameters(decodedUrl, all, unsafeMySQLParameters, "MariaDB");
143+
} else if (decodedUrl.startsWith("jdbc:postgresql")) {
144+
checkUnsafeParameters(decodedUrl, all, unsafePostgresParameters, "PostgreSQL");
145+
}
146+
}
147+
}
148+
149+
private static boolean containsValueIgnoreCase(Map<String, String> map, String value) {
150+
for (String keyValue : map.values()) {
151+
if (keyValue != null && keyValue.equalsIgnoreCase(value)) {
152+
return true;
153+
}
154+
}
155+
return false;
156+
}
157+
79158
public static void closeDataSource(DataSource dataSource) {
80159
if (null != dataSource) {
81160
try {

catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/utils/TestDataSourceUtils.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import javax.sql.DataSource;
2525
import org.apache.commons.dbcp2.BasicDataSource;
2626
import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
27+
import org.apache.gravitino.exceptions.GravitinoRuntimeException;
2728
import org.junit.jupiter.api.Assertions;
2829
import org.junit.jupiter.api.Test;
2930

@@ -42,4 +43,45 @@ public void testCreateDataSource() throws SQLException {
4243
Assertions.assertTrue(dataSource instanceof org.apache.commons.dbcp2.BasicDataSource);
4344
((BasicDataSource) dataSource).close();
4445
}
46+
47+
@Test
48+
public void testRejectMysqlAllowLoadLocalInfile() {
49+
HashMap<String, String> properties = Maps.newHashMap();
50+
properties.put(JdbcConfig.JDBC_DRIVER.getKey(), "com.mysql.cj.jdbc.Driver");
51+
properties.put(
52+
JdbcConfig.JDBC_URL.getKey(), "jdbc:mysql://localhost:3306/test?allowLoadLocalInfile=true");
53+
properties.put(JdbcConfig.USERNAME.getKey(), "test");
54+
properties.put(JdbcConfig.PASSWORD.getKey(), "test");
55+
56+
Assertions.assertThrows(
57+
GravitinoRuntimeException.class, () -> DataSourceUtils.createDataSource(properties));
58+
}
59+
60+
@Test
61+
public void testRejectPostgresSocketFactory() {
62+
HashMap<String, String> properties = Maps.newHashMap();
63+
properties.put(JdbcConfig.JDBC_DRIVER.getKey(), "org.postgresql.Driver");
64+
properties.put(
65+
JdbcConfig.JDBC_URL.getKey(),
66+
"jdbc:postgresql:///test?socketFactory=java.io.FileOutputStream&socketFactoryArg=/tmp/x");
67+
properties.put(JdbcConfig.USERNAME.getKey(), "test");
68+
properties.put(JdbcConfig.PASSWORD.getKey(), "test");
69+
70+
Assertions.assertThrows(
71+
GravitinoRuntimeException.class, () -> DataSourceUtils.createDataSource(properties));
72+
}
73+
74+
@Test
75+
public void testRejectEncodedAllowLoadLocalInfile() {
76+
HashMap<String, String> properties = Maps.newHashMap();
77+
properties.put(JdbcConfig.JDBC_DRIVER.getKey(), "org.postgresql.Driver");
78+
properties.put(
79+
JdbcConfig.JDBC_URL.getKey(),
80+
"jdbc:mysql://address=(host=172.18.0.1)(port=3309)(%61llowLoadLocalInfile=true),172.18.0.1:3306/test");
81+
properties.put(JdbcConfig.USERNAME.getKey(), "test");
82+
properties.put(JdbcConfig.PASSWORD.getKey(), "test");
83+
84+
Assertions.assertThrows(
85+
GravitinoRuntimeException.class, () -> DataSourceUtils.createDataSource(properties));
86+
}
4587
}

0 commit comments

Comments
 (0)