Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ public abstract class SecurityUtils {
private static final CommonVars<String> JDBC_DB2_BLOCKED_PARAMS =
CommonVars$.MODULE$.apply(
"linkis.jdbc.db2.blocked.params",
"clientRerouteServerListJNDIName,enableSeamlessFailover,JNDIName");
// traceLevel / traceFile / traceDirectory / traceFileAppend let the driver write trace
// output to arbitrary paths; blocked alongside the JNDI-related options.
"clientRerouteServerListJNDIName,enableSeamlessFailover,JNDIName,"
+ "traceLevel,traceFile,traceDirectory,traceFileAppend");

private static final CommonVars<String> JDBC_ORACLE_BLOCKED_PARAMS =
CommonVars$.MODULE$.apply(
Expand Down Expand Up @@ -503,7 +506,9 @@ public static void checkJdbcConnParams(
// 2. Host sanity check: reject hosts that smuggle extra URL segments
// (e.g. "host:port/evil?socketFactory=...").
checkHostIsSafe(host);
// 3. Param denylist check (also handles URL-encoded bypass).
// 3. Database sanity check (reject URL option separators in the database segment).
checkDatabaseIsSafe(driverType, database);
// 4. Param denylist check (also handles URL-encoded bypass).
checkDriverParams(driverType, extraParams);
}

Expand Down Expand Up @@ -621,6 +626,63 @@ private static void checkHostIsSafe(String host) {
}
}

/**
* Reject database names that contain URL option separators. The metadata-query / datasource
* manager SqlConnection classes interpolate the user-supplied database/instance segment directly
* into the JDBC URL via String.format, so a value containing a separator character would be
* parsed as an extra URL option. Block the characters that any supported driver family treats as
* a delimiter after the database segment.
*/
private static void checkDatabaseIsSafe(JdbcDriverType driverType, String database) {
if (StringUtils.isBlank(database)) {
return;
}
String trimmed = database.trim();
String forbidden;
switch (driverType) {
case DB2:
// DB2 URL option syntax is "jdbc:db2://h:p/db:opt1=v1;opt2=v2;".
forbidden = ":;?#&";
break;
case ORACLE:
// Oracle thin URL is "jdbc:oracle:thin:@//h:p/service" — ':' and '/' are structural, but
// '?' / '&' would only appear if the caller is abusing the service-name slot to inject
// query-style params. '#' is the fragment anchor and a known smuggling trick.
forbidden = "?#&";
break;
case SQLSERVER:
// SQL Server URL is "jdbc:sqlserver://h:p;databaseName=db;prop=v;" — ';' is the property
// separator, so block it along with the usual query/fragment anchors.
forbidden = ";?#&";
break;
case POSTGRESQL:
case GREENPLUM:
case KINGBASE:
case CLICKHOUSE:
case DM:
case MYSQL:
case STARROCKS:
default:
// For the mysql:// family the database is followed by '?' for query params; for the others
// a '/' selects a path segment. Block all of the URL-meaningful characters.
forbidden = "?#&/";
break;
}
for (int i = 0; i < forbidden.length(); i++) {
char c = forbidden.charAt(i);
if (trimmed.indexOf(c) >= 0) {
throw new LinkisSecurityException(
35000,
"Database name contains forbidden character '"
+ c
+ "' for driver "
+ driverType
+ ": "
+ trimmed);
}
}
}

private static CommonVars<String> getDriverBlockedConfig(JdbcDriverType driverType) {
switch (driverType) {
case POSTGRESQL:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,4 +533,158 @@ public void testBuildSecureProperties_GlobalForceParamsWinOverUserInput() {
Assertions.assertEquals("linkis", props.getProperty("applicationName"));
Assertions.assertEquals("u", props.getProperty("user"));
}

// ----------------------- Database name sanitization tests -----------------------

@Test
public void testGenericCheck_Db2DatabaseInjection() {
// DB2 URL option syntax uses ':' and ';' as delimiters, so a database name containing them
// would be parsed as extra options. checkDatabaseIsSafe must reject them.
Map<String, Object> params = new HashMap<>();
params.put("k1", "v1");
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.DB2, "localhost", 50000, "u", "p", "SAMPLE:traceLevel=1;", params));
// Same with a traceFile payload.
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.DB2,
"localhost",
50000,
"u",
"p",
"SAMPLE:traceFile=/tmp/evil;",
params));
// Single-character URL separators must also be rejected individually.
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.DB2, "localhost", 50000, "u", "p", "db;", params));
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.DB2, "localhost", 50000, "u", "p", "db:opt=v", params));
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.DB2, "localhost", 50000, "u", "p", "db?q=1", params));
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.DB2, "localhost", 50000, "u", "p", "db#frag", params));
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.DB2, "localhost", 50000, "u", "p", "db&k=v", params));
}

@Test
public void testGenericCheck_SqlserverDatabaseInjection() {
// SQL Server property separator is ';', reject it in the database name.
Map<String, Object> params = new HashMap<>();
params.put("k1", "v1");
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.SQLSERVER,
"localhost",
1433,
"u",
"p",
"db;trustServerCertificate=true",
params));
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.SQLSERVER, "localhost", 1433, "u", "p", "db?q=1", params));
}

@Test
public void testGenericCheck_MysqlAndPostgresDatabaseInjection() {
// The mysql:// family uses '?' / '&' for query params and '#' as the fragment anchor.
Map<String, Object> params = new HashMap<>();
params.put("k1", "v1");
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.MYSQL,
"localhost",
3306,
"u",
"p",
"db?autoDeserialize=true",
params));
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.POSTGRESQL,
"localhost",
5432,
"u",
"p",
"db?socketFactory=evil",
params));
// '/' is blocked for the mysql:// family because it selects a different path segment.
Assertions.assertThrows(
LinkisSecurityException.class,
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.POSTGRESQL, "localhost", 5432, "u", "p", "db/other", params));
}

@Test
public void testGenericCheck_BenignDatabaseNamesPass() {
// Sanity: legitimate database names for each driver family must still pass.
Map<String, Object> params = new HashMap<>();
Assertions.assertDoesNotThrow(
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.DB2, "localhost", 50000, "u", "p", "SAMPLE", params));
Assertions.assertDoesNotThrow(
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.ORACLE, "localhost", 1521, "u", "p", "ORCL", params));
Assertions.assertDoesNotThrow(
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.POSTGRESQL, "localhost", 5432, "u", "p", "postgres", params));
Assertions.assertDoesNotThrow(
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.MYSQL, "localhost", 3306, "u", "p", "db_name", params));
Assertions.assertDoesNotThrow(
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.SQLSERVER, "localhost", 1433, "u", "p", "master", params));
// Blank database names are allowed (some drivers use a default).
Assertions.assertDoesNotThrow(
() ->
SecurityUtils.checkJdbcConnParams(
JdbcDriverType.DB2, "localhost", 50000, "u", "p", " ", params));
}

@Test
public void testGenericCheck_Db2TraceParamsInDenylist() {
// The DB2 denylist must reject these even when they arrive via Properties rather than the URL.
assertDriverRejectsParam(JdbcDriverType.DB2, "traceLevel", "1");
assertDriverRejectsParam(JdbcDriverType.DB2, "traceFile", "/tmp/evil.log");
assertDriverRejectsParam(JdbcDriverType.DB2, "traceDirectory", "/tmp");
assertDriverRejectsParam(JdbcDriverType.DB2, "traceFileAppend", "true");
// JNDI-related params must remain in the denylist.
assertDriverRejectsParam(
JdbcDriverType.DB2, "clientRerouteServerListJNDIName", "ldap://evil/exp");
}
}
Loading
Loading