Skip to content

Commit 602467b

Browse files
aiceflowerclaude
andauthored
#AI COMMIT# [SECURITY][DS] Complete CVE-2023-49566 fix for non-MySQL JDBC drivers (#5449)
The CVE-2023-49566 fix shipped earlier only protected the MySQL/StarRocks SqlConnection implementations. The eight other JDBC driver families used by the metadata-query / datasource-manager modules (PostgreSQL, Oracle, SQL Server, ClickHouse, KingBase, Greenplum, DM, DB2) still streamed user- supplied extraParams straight onto the JDBC URL with no allowlist/denylist, so any authenticated user could inject driver-specific dangerous properties: * PG/Greenplum/KingBase: socketFactory + socketFactoryArg -> RCE on drivers below 42.2.25 / 42.3.2 * DB2: clientRerouteServerListJNDIName -> JNDI injection (the original CVE-2023-49566 sink) * Oracle: oracle.net.tns_admin / javax.net.ssl.trustStore -> TLS/TNS hijack * SQL Server: jaasConfigurationName -> JAAS lookup Fix extends the MySQL-only SecurityUtils path to every driver family: * Add JdbcDriverType enum + per-driver denylist/force-params config in SecurityUtils (checkJdbcConnParams(JdbcDriverType, ...) and buildSecureProperties). * All 16 SqlConnection implementations (8 drivers x 2 modules) now call SecurityUtils.checkJdbcConnParams and route through Properties-based DriverManager.getConnection, never URL concatenation. * Add 10 unit tests covering each driver denylist, URL-encoded bypass, host injection, and force-params-wins semantics. Files: linkis-commons/linkis-common/.../utils/JdbcDriverType.java (new) linkis-commons/linkis-common/.../utils/SecurityUtils.java linkis-commons/linkis-common/.../utils/SecurityUtilsTest.java linkis-public-enhancements/linkis-datasource/linkis-datasource-manager/ service/jdbc/.../AbstractSqlConnection.java linkis-public-enhancements/linkis-datasource/linkis-{metadata-query, datasource-manager}/service/jdbc/.../{postgres,oracle,sqlserver, clickhouse,kingbase,greenplum,dm,db2}/SqlConnection.java Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c6c09e7 commit 602467b

20 files changed

Lines changed: 910 additions & 206 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.linkis.common.utils;
19+
20+
/**
21+
* Identifies the JDBC driver family for security-policy dispatch.
22+
*
23+
* <p>Used by {@link SecurityUtils#checkJdbcConnParams(JdbcDriverType, String, Integer, String,
24+
* String, String, java.util.Map)} and {@link SecurityUtils#buildSecureProperties(JdbcDriverType,
25+
* String, String, java.util.Map)} so that each driver family gets its own sensitive-parameter
26+
* denylist and force-set security defaults.
27+
*/
28+
public enum JdbcDriverType {
29+
MYSQL,
30+
POSTGRESQL,
31+
GREENPLUM,
32+
KINGBASE,
33+
ORACLE,
34+
SQLSERVER,
35+
DB2,
36+
CLICKHOUSE,
37+
DM,
38+
STARROCKS
39+
}

‎linkis-commons/linkis-common/src/main/java/org/apache/linkis/common/utils/SecurityUtils.java‎

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,74 @@ public abstract class SecurityUtils {
8282
private static final String BLACKLIST_REGEX =
8383
"autodeserialize|allowloadlocalinfile|allowurlinlocalinfile|allowloadlocalinfileinpath";
8484

85+
// ----------------------- Generic JDBC security layer -----------------------
86+
// The methods below extend CVE-2023-49566 coverage from MySQL-only to every
87+
// JDBC driver family used by the metadata-query / datasource-manager modules.
88+
// They were missing previously, which left PostgreSQL/Oracle/SQLServer/DB2/
89+
// ClickHouse/KingBase/Greenplum/DM streaming user-supplied params straight
90+
// into DriverManager.getConnection with no allowlist/denylist.
91+
92+
/** Master switch for the generic JDBC parameter check (independent of the MySQL switch). */
93+
private static final CommonVars<String> JDBC_SECURITY_CHECK_ENABLE =
94+
CommonVars$.MODULE$.apply("linkis.jdbc.security.check.enable", "true");
95+
96+
/**
97+
* Parameters blocked for every driver family. The '#', '&', '?' characters block URL-injection
98+
* tricks that smuggle extra segments into the JDBC URL itself.
99+
*/
100+
private static final CommonVars<String> JDBC_GLOBAL_BLOCKED_PARAMS =
101+
CommonVars$.MODULE$.apply(
102+
"linkis.jdbc.global.blocked.params",
103+
"autoDeserialize,#,allowLoadLocalInfile,allowLocalInfile,allowUrlInLocalInfile");
104+
105+
/**
106+
* Per-driver denylist. PG-family drivers (PostgreSQL, Greenplum, KingBase) reflectively
107+
* instantiate socketFactory/sslfactory classes -> RCE on drivers below 42.2.25 / 42.3.2. DB2's
108+
* clientRerouteServerListJNDIName is the original CVE-2023-49566 JNDI sink. Oracle's
109+
* tns_admin/trustStore can hijack TLS / TNS configuration. SQL Server's jaasConfigurationName can
110+
* trigger a JAAS lookup.
111+
*/
112+
private static final CommonVars<String> JDBC_POSTGRES_BLOCKED_PARAMS =
113+
CommonVars$.MODULE$.apply(
114+
"linkis.jdbc.postgres.blocked.params",
115+
"socketFactory,socketFactoryArg,sslfactory,sslfactoryarg,sslhostnameverifier,"
116+
+ "loggerLevel,loggerFile");
117+
118+
private static final CommonVars<String> JDBC_DB2_BLOCKED_PARAMS =
119+
CommonVars$.MODULE$.apply(
120+
"linkis.jdbc.db2.blocked.params",
121+
"clientRerouteServerListJNDIName,enableSeamlessFailover,JNDIName");
122+
123+
private static final CommonVars<String> JDBC_ORACLE_BLOCKED_PARAMS =
124+
CommonVars$.MODULE$.apply(
125+
"linkis.jdbc.oracle.blocked.params",
126+
"oracle.net.tns_admin,javax.net.ssl.trustStore,javax.net.ssl.trustStorePassword,"
127+
+ "oracle.net.ssl_url,javax.net.ssl.keyStore");
128+
129+
private static final CommonVars<String> JDBC_SQLSERVER_BLOCKED_PARAMS =
130+
CommonVars$.MODULE$.apply(
131+
"linkis.jdbc.sqlserver.blocked.params", "jaasConfigurationName,jaasApplicationName");
132+
133+
/** Force-set defaults applied to every driver family. Empty map means no override. */
134+
private static final CommonVars<String> JDBC_POSTGRES_FORCE_PARAMS =
135+
CommonVars$.MODULE$.apply("linkis.jdbc.postgres.force.params", "");
136+
137+
private static final CommonVars<String> JDBC_DB2_FORCE_PARAMS =
138+
CommonVars$.MODULE$.apply("linkis.jdbc.db2.force.params", "");
139+
140+
private static final CommonVars<String> JDBC_ORACLE_FORCE_PARAMS =
141+
CommonVars$.MODULE$.apply("linkis.jdbc.oracle.force.params", "");
142+
143+
private static final CommonVars<String> JDBC_SQLSERVER_FORCE_PARAMS =
144+
CommonVars$.MODULE$.apply(
145+
"linkis.jdbc.sqlserver.force.params", "trustServerCertificate=false");
146+
147+
private static final CommonVars<String> JDBC_CLICKHOUSE_FORCE_PARAMS =
148+
CommonVars$.MODULE$.apply("linkis.jdbc.clickhouse.force.params", "");
149+
150+
private static final CommonVars<String> JDBC_DM_FORCE_PARAMS =
151+
CommonVars$.MODULE$.apply("linkis.jdbc.dm.force.params", "");
152+
85153
/**
86154
* check mysql connection params
87155
*
@@ -390,6 +458,264 @@ public static Properties getMysqlSecurityParams() {
390458
return properties;
391459
}
392460

461+
// ----------------------- Generic JDBC API (added for CVE-2023-49566 fix-up)
462+
// -----------------------
463+
464+
/**
465+
* Driver-aware replacement for the MySQL-only {@link #checkJdbcConnParams(String, Integer,
466+
* String, String, String, Map)}.
467+
*
468+
* <p>Validates the same invariants (non-blank host/username, URL-encode loop, denylist match on
469+
* both key and value) but selects the denylist from {@code driverType} instead of always using
470+
* the MySQL one.
471+
*
472+
* @param driverType JDBC driver family
473+
* @param host connection host
474+
* @param port connection port (nullable)
475+
* @param username connection username
476+
* @param password connection password (not inspected; only passed through)
477+
* @param database connection database name (nullable)
478+
* @param extraParams user-supplied params; will be mutated in place (decoded form replaces
479+
* encoded form, sensitive entries removed) so the caller can hand the same map to {@link
480+
* #buildSecureProperties}
481+
*/
482+
public static void checkJdbcConnParams(
483+
JdbcDriverType driverType,
484+
String host,
485+
Integer port,
486+
String username,
487+
String password,
488+
String database,
489+
Map<String, Object> extraParams) {
490+
if (!Boolean.valueOf(JDBC_SECURITY_CHECK_ENABLE.getValue())) {
491+
return;
492+
}
493+
// 1. Basic blank check. Password is allowed to be blank for some drivers.
494+
if (StringUtils.isBlank(host) || StringUtils.isBlank(username)) {
495+
logger.error(
496+
"Invalid jdbc connection params: driverType={}, host={}, username={}, database={}",
497+
driverType,
498+
host,
499+
username,
500+
database);
501+
throw new LinkisSecurityException(35000, "Invalid jdbc connection params.");
502+
}
503+
// 2. Host sanity check: reject hosts that smuggle extra URL segments
504+
// (e.g. "host:port/evil?socketFactory=...").
505+
checkHostIsSafe(host);
506+
// 3. Param denylist check (also handles URL-encoded bypass).
507+
checkDriverParams(driverType, extraParams);
508+
}
509+
510+
/**
511+
* Build a JDBC {@link Properties} bag that is safe to pass to {@link
512+
* java.sql.DriverManager#getConnection(String, java.util.Properties)}.
513+
*
514+
* <p>The contract is identical to the MySQL secure-properties pattern: driver-specific force-set
515+
* security defaults go in first, then user/password, then user-supplied params are layered on top
516+
* but only if their key does not already exist (so the security defaults always win). This
517+
* replaces the unsafe pattern of string-concatenating extraParams onto the JDBC URL.
518+
*/
519+
public static Properties buildSecureProperties(
520+
JdbcDriverType driverType,
521+
String username,
522+
String password,
523+
Map<String, Object> extraParams) {
524+
Properties props = new Properties();
525+
// 1. Driver-specific force params first — these cannot be overridden by user input.
526+
Map<String, Object> forceParams = getDriverForceParams(driverType);
527+
for (Map.Entry<String, Object> entry : forceParams.entrySet()) {
528+
props.setProperty(entry.getKey(), String.valueOf(entry.getValue()));
529+
}
530+
// 2. Credentials.
531+
if (username != null) {
532+
props.setProperty("user", username);
533+
}
534+
if (password != null) {
535+
props.setProperty("password", password);
536+
}
537+
// 3. User params, but never overwrite the force-set keys.
538+
if (extraParams != null) {
539+
for (Map.Entry<String, Object> entry : extraParams.entrySet()) {
540+
if (entry.getKey() == null) {
541+
continue;
542+
}
543+
if (!props.containsKey(entry.getKey())) {
544+
props.setProperty(entry.getKey(), String.valueOf(entry.getValue()));
545+
}
546+
}
547+
}
548+
return props;
549+
}
550+
551+
/** Convenience: just the denylist lookup so callers can self-check before connecting. */
552+
public static List<String> getBlockedParamNames(JdbcDriverType driverType) {
553+
List<String> blocked = new ArrayList<>();
554+
Collections.addAll(blocked, parseCsv(JDBC_GLOBAL_BLOCKED_PARAMS.getValue()));
555+
Collections.addAll(blocked, parseCsv(getDriverBlockedConfig(driverType).getValue()));
556+
return blocked;
557+
}
558+
559+
private static void checkDriverParams(JdbcDriverType driverType, Map<String, Object> paramsMap) {
560+
if (paramsMap == null || paramsMap.isEmpty()) {
561+
return;
562+
}
563+
// URL-decode loop (handles double-encoded bypass) — same trick as the MySQL path.
564+
String paramUrl =
565+
paramsMap.entrySet().stream()
566+
.map(e -> String.join(EQUAL_SIGN, e.getKey(), String.valueOf(e.getValue())))
567+
.collect(Collectors.joining(AND_SYMBOL));
568+
try {
569+
while (paramUrl.contains("%")) {
570+
String decoded = URLDecoder.decode(paramUrl, "UTF-8");
571+
if (decoded.equals(paramUrl)) {
572+
break;
573+
}
574+
paramUrl = decoded;
575+
}
576+
} catch (UnsupportedEncodingException e) {
577+
throw new LinkisSecurityException(35000, "jdbc connection url decode error: " + e);
578+
}
579+
// Rebuild the params map from the decoded form so callers see the canonical shape.
580+
Map<String, Object> decoded = parseParamUrlToMap(paramUrl);
581+
paramsMap.clear();
582+
paramsMap.putAll(decoded);
583+
584+
// Denylist check. Match on either key or value, case-insensitive, substring match so
585+
// "loggerFile" still catches "loggerfile" typos and similar evasions.
586+
List<String> blocked = getBlockedParamNames(driverType);
587+
Iterator<Map.Entry<String, Object>> iterator = paramsMap.entrySet().iterator();
588+
while (iterator.hasNext()) {
589+
Map.Entry<String, Object> entry = iterator.next();
590+
String key = entry.getKey();
591+
Object value = entry.getValue();
592+
if (StringUtils.isBlank(key) || value == null || StringUtils.isBlank(value.toString())) {
593+
// Drop blank entries — they are noise.
594+
iterator.remove();
595+
continue;
596+
}
597+
if (containsAnyToken(key, value.toString(), blocked)) {
598+
logger.warn(
599+
"Sensitive jdbc param blocked: driverType={}, key={}, value={}",
600+
driverType,
601+
key,
602+
value);
603+
throw new LinkisSecurityException(
604+
35000, "Invalid jdbc connection parameter for driver " + driverType + ": key=" + key);
605+
}
606+
}
607+
}
608+
609+
/**
610+
* Reject hosts that contain URL-meaningful characters. A malicious host like
611+
* "evil.com:5432/db?socketFactory=x" would otherwise smuggle params past the denylist because
612+
* they live in the URL rather than in extraParams.
613+
*/
614+
private static void checkHostIsSafe(String host) {
615+
if (StringUtils.isBlank(host)) {
616+
return;
617+
}
618+
String trimmed = host.trim();
619+
if (trimmed.contains("?") || trimmed.contains("#") || trimmed.contains("&")) {
620+
throw new LinkisSecurityException(35000, "Host contains forbidden URL character: " + trimmed);
621+
}
622+
}
623+
624+
private static CommonVars<String> getDriverBlockedConfig(JdbcDriverType driverType) {
625+
switch (driverType) {
626+
case POSTGRESQL:
627+
case GREENPLUM:
628+
case KINGBASE:
629+
return JDBC_POSTGRES_BLOCKED_PARAMS;
630+
case DB2:
631+
return JDBC_DB2_BLOCKED_PARAMS;
632+
case ORACLE:
633+
return JDBC_ORACLE_BLOCKED_PARAMS;
634+
case SQLSERVER:
635+
return JDBC_SQLSERVER_BLOCKED_PARAMS;
636+
case MYSQL:
637+
case STARROCKS:
638+
case CLICKHOUSE:
639+
case DM:
640+
default:
641+
// MySQL keeps using its own MYSQL_SENSITIVE_PARAMS path for backwards compatibility;
642+
// ClickHouse/DM fall through with just the global denylist.
643+
return JDBC_GLOBAL_BLOCKED_PARAMS;
644+
}
645+
}
646+
647+
private static Map<String, Object> getDriverForceParams(JdbcDriverType driverType) {
648+
CommonVars<String> source;
649+
switch (driverType) {
650+
case POSTGRESQL:
651+
case GREENPLUM:
652+
case KINGBASE:
653+
source = JDBC_POSTGRES_FORCE_PARAMS;
654+
break;
655+
case DB2:
656+
source = JDBC_DB2_FORCE_PARAMS;
657+
break;
658+
case ORACLE:
659+
source = JDBC_ORACLE_FORCE_PARAMS;
660+
break;
661+
case SQLSERVER:
662+
source = JDBC_SQLSERVER_FORCE_PARAMS;
663+
break;
664+
case CLICKHOUSE:
665+
source = JDBC_CLICKHOUSE_FORCE_PARAMS;
666+
break;
667+
case DM:
668+
source = JDBC_DM_FORCE_PARAMS;
669+
break;
670+
case MYSQL:
671+
case STARROCKS:
672+
default:
673+
return new LinkedHashMap<>();
674+
}
675+
return parseParamUrlToMap(source.getValue());
676+
}
677+
678+
private static boolean containsAnyToken(String key, String value, List<String> tokens) {
679+
String lowerKey = key.toLowerCase();
680+
String lowerValue = value.toLowerCase();
681+
for (String token : tokens) {
682+
if (StringUtils.isBlank(token)) {
683+
continue;
684+
}
685+
String lower = token.toLowerCase();
686+
if (lowerKey.contains(lower) || lowerValue.contains(lower)) {
687+
return true;
688+
}
689+
}
690+
return false;
691+
}
692+
693+
private static String[] parseCsv(String csv) {
694+
if (StringUtils.isBlank(csv)) {
695+
return new String[0];
696+
}
697+
return csv.split(COMMA);
698+
}
699+
700+
private static Map<String, Object> parseParamUrlToMap(String paramsUrl) {
701+
Map<String, Object> map = new LinkedHashMap<>();
702+
if (StringUtils.isBlank(paramsUrl)) {
703+
return map;
704+
}
705+
for (String param : paramsUrl.split(AND_SYMBOL)) {
706+
int idx = param.indexOf(EQUAL_SIGN);
707+
if (idx < 0) {
708+
continue;
709+
}
710+
String k = param.substring(0, idx);
711+
String v = param.substring(idx + 1);
712+
if (StringUtils.isNotBlank(k)) {
713+
map.put(k, v);
714+
}
715+
}
716+
return map;
717+
}
718+
393719
/**
394720
* Check if the path has a relative path
395721
*

0 commit comments

Comments
 (0)