mirror of
http://git.pig4cloud.com/pig/pigx.git
synced 2026-11-08 04:39:00 +08:00
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:
1 parent
e4bda833bc
commit
daad35902e
4 files changed
+301
-8
No files matched your search
@@ -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
|
||||
|
||||
|
||||
+2
@@ -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())) {
|
||||
}
|
||||
|
||||
+204
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+91
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user