From daad35902e3879ab444a4360d3069f5c724fd1d9 Mon Sep 17 00:00:00 2001 From: PIGCLOUD Date: Tue, 18 Aug 2026 09:17:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(pigx-codegen):=20=E6=96=B0=E5=A2=9EJDBC=20?= =?UTF-8?q?URL=E5=AE=89=E5=85=A8=E6=A0=A1=E9=AA=8C=E5=99=A8=EF=BC=8C?= =?UTF-8?q?=E9=98=B2=E6=AD=A2=E5=8D=B1=E9=99=A9=E5=8F=82=E6=95=B0=E6=B3=A8?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增JdbcUrlSecurityValidator,支持MySQL、PostgreSQL、HighGo、SQL Server、Oracle、DB2、DM的JDBC URL危险参数检测 - 在GenDatasourceConfServiceImpl中保存数据源配置时进行安全校验,拒绝包含不安全参数的连接 - 新增GenDatasourceConfServiceImplTests测试用例,覆盖安全与安全的URL场景 - 更新AGENTS.md,调整构建命令描述 (cherry picked from commit cabe0775957e6274cf3c31760724a5ee87fdd743) --- AGENTS.md | 12 +- .../impl/GenDatasourceConfServiceImpl.java | 2 + .../util/JdbcUrlSecurityValidator.java | 204 ++++++++++++++++++ .../GenDatasourceConfServiceImplTests.java | 91 ++++++++ 4 files changed, 301 insertions(+), 8 deletions(-) create mode 100644 pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/util/JdbcUrlSecurityValidator.java create mode 100644 pigx-visual/pigx-codegen/src/test/java/com/pig4cloud/pigx/codegen/service/impl/GenDatasourceConfServiceImplTests.java diff --git a/AGENTS.md b/AGENTS.md index 8a11573db..09e898e88 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,19 +11,15 @@ layout. ## Build, Test, and Development Commands -- `mvn -T 1C clean install -DskipTests` compiles all modules with the managed BOM; add `-pl pigx-gateway -am` to limit - the build to a single service. -- `mvn -pl pigx-gateway spring-boot:run` (swap the module name as needed) starts a service with dev profiles for live - reloads. -- `docker-compose up -d pigx-mysql pigx-redis pigx-register` brings up local infrastructure; run - `docker-compose up --build pigx-auth pigx-upms` when you need the auth stack. -- `mvn -pl pigx-knowledge test` executes module tests; append `-DskipITs` to bypass integration suites. +Focused Maven commands are allowed only for test execution. They must not be used to package, start, or restart +services. ## Testing Guidelines Use `spring-boot-starter-test` (JUnit 5, AssertJ, Mockito) for unit and slice tests. Name classes `*Tests.java` and colocate fixtures in `src/test/resources`. Target >70% line coverage on the service layer that guards authentication, -gateway filters, and approval flows. Run `mvn verify` before opening a PR to exercise the full plugin stack. +gateway filters, and approval flows. Before opening a PR, run the focused tests required by the change, then perform +the required runtime validation. Do not use `mvn verify` as a build shortcut unless the user explicitly requests it. ## Commit & Pull Request Guidelines diff --git a/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/service/impl/GenDatasourceConfServiceImpl.java b/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/service/impl/GenDatasourceConfServiceImpl.java index c231f4986..0ce259d45 100755 --- a/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/service/impl/GenDatasourceConfServiceImpl.java +++ b/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/service/impl/GenDatasourceConfServiceImpl.java @@ -27,6 +27,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.pig4cloud.pigx.codegen.entity.GenDatasourceConf; import com.pig4cloud.pigx.codegen.mapper.GenDatasourceConfMapper; import com.pig4cloud.pigx.codegen.service.GenDatasourceConfService; +import com.pig4cloud.pigx.codegen.util.JdbcUrlSecurityValidator; import com.pig4cloud.pigx.common.core.util.SpringContextHolder; import com.pig4cloud.pigx.common.datasource.util.DsConfTypeEnum; import com.pig4cloud.pigx.common.datasource.util.DsTypeEnum; @@ -167,6 +168,7 @@ public class GenDatasourceConfServiceImpl extends ServiceImpl MYSQL_UNSAFE_PARAMETERS = normalizedSet( + PropertyKey.allowLoadLocalInfile.getKeyName(), PropertyKey.allowLoadLocalInfileInPath.getKeyName(), + PropertyKey.allowUrlInLocalInfile.getKeyName(), PropertyKey.authenticationPlugins.getKeyName(), + PropertyKey.authenticationOpenidConnectCallbackHandler.getKeyName(), + PropertyKey.authenticationWebAuthnCallbackHandler.getKeyName(), + PropertyKey.clientCertificateKeyStoreUrl.getKeyName(), PropertyKey.defaultAuthenticationPlugin.getKeyName(), + PropertyKey.connectionLifecycleInterceptors.getKeyName(), PropertyKey.exceptionInterceptors.getKeyName(), + PropertyKey.queryInterceptors.getKeyName(), PropertyKey.idTokenFile.getKeyName(), + PropertyKey.ociConfigFile.getKeyName(), PropertyKey.propertiesTransform.getKeyName(), + PropertyKey.socketFactory.getKeyName(), PropertyKey.logger.getKeyName(), + PropertyKey.profilerEventHandler.getKeyName(), PropertyKey.queryInfoCacheFactory.getKeyName(), + PropertyKey.serverConfigCacheFactory.getKeyName(), PropertyKey.serverRSAPublicKeyFile.getKeyName(), + PropertyKey.trustCertificateKeyStoreUrl.getKeyName(), "autoDeserialize", "statementInterceptors"); + + private static final Set POSTGRESQL_UNSAFE_PARAMETERS = normalizedSet( + PGProperty.AUTHENTICATION_PLUGIN_CLASS_NAME.getName(), PGProperty.SERVICE.getName(), + PGProperty.SSL_CERT.getName(), PGProperty.SSL_KEY.getName(), PGProperty.SSL_ROOT_CERT.getName(), + "loggerFile", PGProperty.SOCKET_FACTORY.getName(), PGProperty.SSL_FACTORY.getName(), + PGProperty.SSL_HOSTNAME_VERIFIER.getName(), PGProperty.SSL_PASSWORD_CALLBACK.getName(), + PGProperty.XML_FACTORY_FACTORY.getName()); + + private static final Set HIGHGO_UNSAFE_PARAMETERS = normalizedSet( + com.highgo.jdbc.PGProperty.BULKLOAD_CTL_FILE.getName(), com.highgo.jdbc.PGProperty.LOGGER_FILE.getName(), + com.highgo.jdbc.PGProperty.SSL_CERT.getName(), com.highgo.jdbc.PGProperty.SSL_ENC_CERT.getName(), + com.highgo.jdbc.PGProperty.SSL_ENC_KEY.getName(), com.highgo.jdbc.PGProperty.SSL_KEY.getName(), + com.highgo.jdbc.PGProperty.SSL_ROOT_CERT.getName(), com.highgo.jdbc.PGProperty.SOCKET_FACTORY.getName(), + com.highgo.jdbc.PGProperty.SSL_FACTORY.getName(), + com.highgo.jdbc.PGProperty.SSL_HOSTNAME_VERIFIER.getName(), + com.highgo.jdbc.PGProperty.SSL_PASSWORD_CALLBACK.getName(), + com.highgo.jdbc.PGProperty.XML_FACTORY_FACTORY.getName()); + + private static final Set SQL_SERVER_UNSAFE_PARAMETERS = normalizedSet("accessTokenCallbackClass", + "socketFactoryClass", "trustManagerClass", "clientCertificate", "clientKey", "keyStoreLocation", + "serverCertificate", "trustStore"); + + private static final Set ORACLE_UNSAFE_PARAMETERS = normalizedSet("TNS_ADMIN", + OracleConnection.CONNECTION_PROPERTY_TNS_ADMIN, OracleConnection.CONNECTION_PROPERTY_WALLET_LOCATION, + OracleConnection.CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_KEYSTORE, + OracleConnection.CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_TRUSTSTORE, + OracleConnection.CONNECTION_PROPERTY_ONS_WALLET_FILE, "MY_WALLET_DIRECTORY"); + + private static final Set DB2_UNSAFE_PARAMETERS = normalizedSet("pluginClassName", "sslCertLocation", + "sslTrustStoreLocation", "sslKeyStoreLocation", "traceFile", "dbPath", "pLoadLoPath"); + + private static final Set DM_UNSAFE_PARAMETERS = normalizedSet("cipherPath", "customFilter", "feFilePath", + "kerberosLoginConfPath", "loginCertificate", "logDir", "sslFilesPath", "statDir", "unixSocketFile"); + + private JdbcUrlSecurityValidator() { + } + + /** + * 校验 JDBC URL 中是否包含危险参数。 + * @param url JDBC URL + */ + public static void validate(String url) { + if (StrUtil.isBlank(url)) { + return; + } + + String normalizedUrl = url.toLowerCase(Locale.ROOT); + try { + if (normalizedUrl.startsWith("jdbc:mysql")) { + rejectUnsafeParameters(mysqlParameterNames(url), MYSQL_UNSAFE_PARAMETERS); + } + else if (normalizedUrl.startsWith("jdbc:postgresql:")) { + // PG 的 service 参数会触发驱动读取本地 pg_service.conf,必须在调用驱动解析器前拦截。 + rejectUnsafeParameters(parameterNames(url, QUERY_PARAMETER_SEPARATOR), POSTGRESQL_UNSAFE_PARAMETERS); + rejectUnsafeParameters(postgresqlParameterNames(url), POSTGRESQL_UNSAFE_PARAMETERS); + } + else if (normalizedUrl.startsWith("jdbc:highgo:")) { + rejectUnsafeParameters(highgoParameterNames(url), HIGHGO_UNSAFE_PARAMETERS); + } + else if (normalizedUrl.startsWith("jdbc:sqlserver:")) { + rejectUnsafeParameters(sqlServerParameterNames(url), SQL_SERVER_UNSAFE_PARAMETERS); + } + else if (normalizedUrl.startsWith("jdbc:oracle:")) { + rejectUnsafeParameters(genericParameterNames(url), ORACLE_UNSAFE_PARAMETERS); + } + else if (normalizedUrl.startsWith("jdbc:db2:")) { + rejectUnsafeParameters(genericParameterNames(url), DB2_UNSAFE_PARAMETERS); + } + else if (normalizedUrl.startsWith("jdbc:dm:")) { + rejectUnsafeParameters(genericParameterNames(url), DM_UNSAFE_PARAMETERS); + } + } + catch (CheckedException e) { + throw e; + } + catch (Exception e) { + throw new CheckedException("JDBC URL 参数格式不合法"); + } + } + + private static Collection mysqlParameterNames(String url) { + ConnectionUrlParser parser = ConnectionUrlParser.parseConnectionString(url); + return Stream + .concat(parser.getProperties().keySet().stream(), + parser.getHosts().stream().flatMap(host -> host.getHostProperties().keySet().stream())) + .toList(); + } + + private static Collection postgresqlParameterNames(String url) { + Properties properties = Objects.requireNonNull(org.postgresql.Driver.parseURL(url, new Properties())); + return properties.stringPropertyNames(); + } + + private static Collection highgoParameterNames(String url) { + Properties properties = Objects.requireNonNull(com.highgo.jdbc.Driver.parseURL(url, new Properties())); + return properties.stringPropertyNames(); + } + + private static Collection sqlServerParameterNames(String url) throws Exception { + return Arrays.stream(new SQLServerDriver().getPropertyInfo(url, new Properties())) + .filter(property -> StrUtil.isNotBlank(property.value)) + .map(property -> property.name) + .toList(); + } + + private static Collection genericParameterNames(String url) { + return parameterNames(url, GENERIC_PARAMETER_SEPARATOR); + } + + private static Collection parameterNames(String url, Pattern parameterSeparator) { + Set parameterNames = new HashSet<>(); + for (String segment : parameterSeparator.split(url)) { + int separatorIndex = segment.indexOf('='); + if (separatorIndex < 0) { + continue; + } + parameterNames.add(URLDecoder.decode(segment.substring(0, separatorIndex), StandardCharsets.UTF_8).trim()); + } + return parameterNames; + } + + private static void rejectUnsafeParameters(Collection parameterNames, Set unsafeParameters) { + boolean containsUnsafeParameter = parameterNames.stream() + .map(JdbcUrlSecurityValidator::normalize) + .anyMatch(unsafeParameters::contains); + if (containsUnsafeParameter) { + throw new CheckedException("JDBC URL 包含不安全参数"); + } + } + + private static Set normalizedSet(String... parameters) { + return Arrays.stream(parameters) + .map(JdbcUrlSecurityValidator::normalize) + .collect(Collectors.toUnmodifiableSet()); + } + + private static String normalize(String parameter) { + return parameter.trim().toLowerCase(Locale.ROOT); + } + +} diff --git a/pigx-visual/pigx-codegen/src/test/java/com/pig4cloud/pigx/codegen/service/impl/GenDatasourceConfServiceImplTests.java b/pigx-visual/pigx-codegen/src/test/java/com/pig4cloud/pigx/codegen/service/impl/GenDatasourceConfServiceImplTests.java new file mode 100644 index 000000000..67e2b97d3 --- /dev/null +++ b/pigx-visual/pigx-codegen/src/test/java/com/pig4cloud/pigx/codegen/service/impl/GenDatasourceConfServiceImplTests.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ +package com.pig4cloud.pigx.codegen.service.impl; + +import com.baomidou.dynamic.datasource.creator.DataSourceCreator; +import com.pig4cloud.pigx.codegen.entity.GenDatasourceConf; +import com.pig4cloud.pigx.codegen.util.JdbcUrlSecurityValidator; +import com.pig4cloud.pigx.common.core.exception.CheckedException; +import org.jasypt.encryption.StringEncryptor; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +/** + * 数据源配置安全校验测试。 + * + * @author lengleng + */ +class GenDatasourceConfServiceImplTests { + + @ParameterizedTest + @ValueSource(strings = { "allowLoadLocalInfile=true", "allowUrlInLocalInfile=true", "allowLoadLocalInfileInPath=/", + "ALLOWLOADLOCALINFILE=true", "allow%4CoadLocalInfile=true" }) + void saveDsByEncShouldRejectUnsafeMysqlParametersBeforeConnecting(String query) { + GenDatasourceConfServiceImpl service = new GenDatasourceConfServiceImpl(mock(StringEncryptor.class), + mock(DataSourceCreator.class)); + GenDatasourceConf conf = new GenDatasourceConf(); + conf.setName("unsafe"); + conf.setDsType("mysql"); + conf.setConfType(1); + conf.setUrl("jdbc:mysql://127.0.0.1:1/test?" + query); + conf.setUsername("test"); + conf.setPassword("test"); + + assertThatThrownBy(() -> service.saveDsByEnc(conf)).isInstanceOf(CheckedException.class) + .hasMessage("JDBC URL 包含不安全参数"); + } + + @ParameterizedTest + @ValueSource(strings = { "jdbc:mysql://127.0.0.1:3306/test?allowLoadLocalInfile=true", + "jdbc:mysql://127.0.0.1:3306/test?allowUrlInLocalInfile=true", + "jdbc:mysql://127.0.0.1:3306/test?allowLoadLocalInfileInPath=/", + "jdbc:mysql://127.0.0.1:3306/test?propertiesTransform=example.UnsafeTransform", + "jdbc:mysql://127.0.0.1:3306/test?serverRSAPublicKeyFile=/tmp/server-key.pem", + "jdbc:mysql://address=(host=127.0.0.1)(allowLoadLocalInfile=true)/test", + "jdbc:mysql://(host=127.0.0.1,allowUrlInLocalInfile=true)/test", + "jdbc:postgresql://127.0.0.1:5432/test?socketFactory=example.UnsafeSocketFactory", + "jdbc:postgresql://127.0.0.1:5432/test?service=unsafe-service", + "jdbc:postgresql://127.0.0.1:5432/test?sslkey=/tmp/client-key.pk8", + "jdbc:highgo://127.0.0.1:5866/test?xmlFactoryFactory=LEGACY_INSECURE", + "jdbc:highgo://127.0.0.1:5866/test?bulkloadCtlFile=/tmp/load.ctl", + "jdbc:sqlserver://127.0.0.1:1433;trustManagerClass=example.UnsafeTrustManager", + "jdbc:sqlserver://127.0.0.1:1433;clientCertificate=/tmp/client.pem", + "jdbc:oracle:thin:@127.0.0.1:1521:test?TNS_ADMIN=/tmp/wallet", + "jdbc:oracle:thin:@127.0.0.1:1521:test?oracle.net.wallet_location=/tmp/wallet", + "jdbc:db2://127.0.0.1:50000/test:pluginClassName=example.UnsafePlugin;", + "jdbc:db2://127.0.0.1:50000/test:traceFile=/tmp/db2.log;", + "jdbc:dm://127.0.0.1:5236/test?customFilter=example.UnsafeFilter" }) + void validateJdbcUrlShouldRejectUnsafeParameters(String url) { + assertThatThrownBy(() -> JdbcUrlSecurityValidator.validate(url)).isInstanceOf(CheckedException.class) + .hasMessage("JDBC URL 包含不安全参数"); + } + + @ParameterizedTest + @ValueSource(strings = { "jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&allowMultiQueries=true", + "jdbc:postgresql://127.0.0.1:5432/test?ssl=true", "jdbc:highgo://127.0.0.1:5866/test?connectTimeout=10", + "jdbc:sqlserver://127.0.0.1:1433;databaseName=test;encrypt=true", "jdbc:oracle:thin:@127.0.0.1:1521:test", + "jdbc:db2://127.0.0.1:50000/test:user=test;", "jdbc:dm://127.0.0.1:5236/test?schema=test", + "jdbc:mysql://127.0.0.1:3306/test?note=allowLoadLocalInfile%3Dtrue" }) + void validateJdbcUrlShouldAllowSafeParameters(String url) { + assertThatCode(() -> JdbcUrlSecurityValidator.validate(url)).doesNotThrowAnyException(); + } + +}