feat(pigx-codegen): 新增JDBC URL安全校验器,防止危险参数注入

- 新增JdbcUrlSecurityValidator,支持MySQL、PostgreSQL、HighGo、SQL Server、Oracle、DB2、DM的JDBC URL危险参数检测
- 在GenDatasourceConfServiceImpl中保存数据源配置时进行安全校验,拒绝包含不安全参数的连接
- 新增GenDatasourceConfServiceImplTests测试用例,覆盖安全与安全的URL场景
- 更新AGENTS.md,调整构建命令描述

(cherry picked from commit cabe0775957e6274cf3c31760724a5ee87fdd743)
This commit is contained in:
PIGCLOUD committed 2026-11-03 14:54:37 +08:00
1 parent e4bda833bc
commit daad35902e
4 files changed
+301 -8

No files matched your search

+4 -8
View File
@@ -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
@@ -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<GenDatasourceConfM
}
conf.setUrl(url);
JdbcUrlSecurityValidator.validate(url);
try (Connection connection = DriverManager.getConnection(url, conf.getUsername(), conf.getPassword())) {
}
@@ -0,0 +1,204 @@
/*
* 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.util;
import cn.hutool.core.util.StrUtil;
import com.microsoft.sqlserver.jdbc.SQLServerDriver;
import com.mysql.cj.conf.ConnectionUrlParser;
import com.mysql.cj.conf.PropertyKey;
import com.pig4cloud.pigx.common.core.exception.CheckedException;
import oracle.jdbc.OracleConnection;
import org.postgresql.PGProperty;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* JDBC URL 危险参数校验器。
*
* @author lengleng
*/
public final class JdbcUrlSecurityValidator {
private static final Pattern GENERIC_PARAMETER_SEPARATOR = Pattern.compile("[?&;,():]");
private static final Pattern QUERY_PARAMETER_SEPARATOR = Pattern.compile("[?&]");
private static final Set<String> 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<String> 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<String> 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<String> SQL_SERVER_UNSAFE_PARAMETERS = normalizedSet("accessTokenCallbackClass",
"socketFactoryClass", "trustManagerClass", "clientCertificate", "clientKey", "keyStoreLocation",
"serverCertificate", "trustStore");
private static final Set<String> 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<String> DB2_UNSAFE_PARAMETERS = normalizedSet("pluginClassName", "sslCertLocation",
"sslTrustStoreLocation", "sslKeyStoreLocation", "traceFile", "dbPath", "pLoadLoPath");
private static final Set<String> 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<String> 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<String> postgresqlParameterNames(String url) {
Properties properties = Objects.requireNonNull(org.postgresql.Driver.parseURL(url, new Properties()));
return properties.stringPropertyNames();
}
private static Collection<String> highgoParameterNames(String url) {
Properties properties = Objects.requireNonNull(com.highgo.jdbc.Driver.parseURL(url, new Properties()));
return properties.stringPropertyNames();
}
private static Collection<String> 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<String> genericParameterNames(String url) {
return parameterNames(url, GENERIC_PARAMETER_SEPARATOR);
}
private static Collection<String> parameterNames(String url, Pattern parameterSeparator) {
Set<String> 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<String> parameterNames, Set<String> unsafeParameters) {
boolean containsUnsafeParameter = parameterNames.stream()
.map(JdbcUrlSecurityValidator::normalize)
.anyMatch(unsafeParameters::contains);
if (containsUnsafeParameter) {
throw new CheckedException("JDBC URL 包含不安全参数");
}
}
private static Set<String> 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);
}
}
@@ -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();
}
}