@@ -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