解决【issues/9840】临时加固表字典敏感字段访问风险

1. 禁止字典接口查询或通过条件、排序使用敏感字段。
  2. 当前限制用户密码、数据源账号密码、OpenAPI密钥及AI模型凭据。
  3. 字典条件增加结构化校验,拦截子查询、函数及附加SQL。
  4. 增加前端敏感字段访问测试示例。

  说明:本次为临时方案,通过敏感表和敏感字段清单进行拦截,暂未调整现有动态SQL架构
This commit is contained in:
zhangdaiscott 2026-08-26 10:20:34 +08:00
parent 898cd0c309
commit f271cf3420
5 changed files with 460 additions and 13 deletions

View File

@ -0,0 +1,253 @@
package org.jeecg.common.util;
import net.sf.jsqlparser.expression.BinaryExpression;
import net.sf.jsqlparser.expression.DoubleValue;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.SignedExpression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.Between;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.GreaterThan;
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
import net.sf.jsqlparser.expression.operators.relational.MinorThan;
import net.sf.jsqlparser.expression.operators.relational.MinorThanEquals;
import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParser;
import net.sf.jsqlparser.parser.CCJSqlParserConstants;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 前端字典条件 SQL 结构校验工具
*
* @author scott
* @since 2026-08-25 issues/9840 字典过滤条件 SQL 注入防护
*/
public final class DictSqlConditionCheckUtil {
private DictSqlConditionCheckUtil() {
}
/**
* 校验字典条件语法并返回条件中使用的字段
*
* @param value 字典过滤条件
* @return 条件字段集合
* @throws IllegalArgumentException 条件包含不支持或不安全的 SQL 结构
*/
public static Set<String> checkAndGetFields(String value) {
if (value == null || value.trim().isEmpty()) {
return Set.of();
}
String trimmed = value.trim();
Set<String> conditionFields = new LinkedHashSet<>();
try {
if (containsSqlComment(trimmed)) {
throw new IllegalArgumentException("字典过滤条件不允许包含SQL注释");
}
int orderByIndex = findTopLevelOrderBy(trimmed);
String conditionSql = orderByIndex < 0 ? trimmed : trimmed.substring(0, orderByIndex).trim();
String orderBySql = orderByIndex < 0 ? null
: trimmed.substring(orderByIndex).replaceFirst("(?i)^order\\s+by\\s+", "").trim();
if ((!conditionSql.isEmpty() && !validateCondition(conditionSql, conditionFields))
|| (orderBySql != null && !validateOrderBy(orderBySql, conditionFields))) {
throw new IllegalArgumentException("不支持的字典过滤条件");
}
return conditionFields;
} catch (Exception e) {
throw new IllegalArgumentException("不支持的字典过滤条件", e);
}
}
private static boolean validateCondition(String value, Set<String> conditionFields) throws Exception {
CCJSqlParser parser = CCJSqlParserUtil.newParser(value);
Expression expression = parser.Expression();
return parser.getNextToken().kind == CCJSqlParserConstants.EOF
&& validateExpression(expression, conditionFields);
}
private static boolean validateOrderBy(String value, Set<String> conditionFields) throws Exception {
if (value.isEmpty()) {
return false;
}
for (String orderItem : value.split(",")) {
String[] parts = orderItem.trim().split("\\s+");
if (parts.length == 0 || parts.length > 2
|| (parts.length == 2 && !"ASC".equalsIgnoreCase(parts[1]) && !"DESC".equalsIgnoreCase(parts[1]))) {
return false;
}
CCJSqlParser parser = CCJSqlParserUtil.newParser(parts[0]);
Expression expression = parser.Expression();
if (parser.getNextToken().kind != CCJSqlParserConstants.EOF
|| !addConditionField(expression, conditionFields)) {
return false;
}
}
return true;
}
/**
* 定位字符串和括号之外的 ORDER BY避免误判条件值中的普通文本
*/
private static int findTopLevelOrderBy(String value) {
char quote = 0;
int depth = 0;
for (int i = 0; i < value.length(); i++) {
char current = value.charAt(i);
if (quote != 0) {
if (current == '\\' && i + 1 < value.length()) {
i++;
} else if (current == quote) {
if (i + 1 < value.length() && value.charAt(i + 1) == quote) {
i++;
} else {
quote = 0;
}
}
continue;
}
if (current == '\'' || current == '"' || current == '`') {
quote = current;
} else if (current == '(') {
depth++;
} else if (current == ')') {
depth--;
} else if (depth == 0 && isKeyword(value, i, "order")) {
int byIndex = i + "order".length();
if (byIndex < value.length() && Character.isWhitespace(value.charAt(byIndex))) {
while (byIndex < value.length() && Character.isWhitespace(value.charAt(byIndex))) {
byIndex++;
}
if (isKeyword(value, byIndex, "by")) {
return i;
}
}
}
}
return -1;
}
private static boolean isKeyword(String value, int start, String keyword) {
if (start < 0 || start + keyword.length() > value.length()
|| !value.regionMatches(true, start, keyword, 0, keyword.length())) {
return false;
}
boolean leftBoundary = start == 0 || !Character.isJavaIdentifierPart(value.charAt(start - 1));
int end = start + keyword.length();
boolean rightBoundary = end == value.length() || !Character.isJavaIdentifierPart(value.charAt(end));
return leftBoundary && rightBoundary;
}
/**
* 仅识别字符串和引用标识符之外的 SQL 注释避免误伤普通文本中的注释符号
*/
private static boolean containsSqlComment(String value) {
char quote = 0;
for (int i = 0; i < value.length(); i++) {
char current = value.charAt(i);
if (quote != 0) {
if (current == '\\' && i + 1 < value.length()) {
i++;
} else if (current == quote) {
if (i + 1 < value.length() && value.charAt(i + 1) == quote) {
i++;
} else {
quote = 0;
}
}
continue;
}
if (current == '\'' || current == '"' || current == '`') {
quote = current;
continue;
}
if (i + 1 < value.length()
&& ((current == '-' && value.charAt(i + 1) == '-')
|| (current == '/' && value.charAt(i + 1) == '*'))) {
return true;
}
}
return false;
}
private static boolean validateExpression(Expression expression, Set<String> conditionFields) {
if (expression instanceof ParenthesedExpressionList<?> expressionList) {
return expressionList.size() == 1 && validateExpression(expressionList.get(0), conditionFields);
}
if (expression instanceof AndExpression || expression instanceof OrExpression) {
BinaryExpression logicalExpression = (BinaryExpression) expression;
return validateExpression(logicalExpression.getLeftExpression(), conditionFields)
&& validateExpression(logicalExpression.getRightExpression(), conditionFields);
}
if (isComparisonExpression(expression)) {
BinaryExpression comparison = (BinaryExpression) expression;
return addConditionField(comparison.getLeftExpression(), conditionFields)
&& isLiteral(comparison.getRightExpression());
}
if (expression instanceof LikeExpression likeExpression) {
return addConditionField(likeExpression.getLeftExpression(), conditionFields)
&& likeExpression.getRightExpression() instanceof StringValue
&& (likeExpression.getEscape() == null || likeExpression.getEscape() instanceof StringValue);
}
if (expression instanceof InExpression inExpression) {
return addConditionField(inExpression.getLeftExpression(), conditionFields)
&& isLiteralList(inExpression.getRightExpression());
}
if (expression instanceof Between between) {
return addConditionField(between.getLeftExpression(), conditionFields)
&& isLiteral(between.getBetweenExpressionStart())
&& isLiteral(between.getBetweenExpressionEnd());
}
if (expression instanceof IsNullExpression isNullExpression) {
return addConditionField(isNullExpression.getLeftExpression(), conditionFields);
}
return false;
}
private static boolean isComparisonExpression(Expression expression) {
return expression instanceof EqualsTo
|| expression instanceof NotEqualsTo
|| expression instanceof GreaterThan
|| expression instanceof GreaterThanEquals
|| expression instanceof MinorThan
|| expression instanceof MinorThanEquals;
}
private static boolean addConditionField(Expression expression, Set<String> conditionFields) {
if (!(expression instanceof Column column)) {
return false;
}
conditionFields.add(column.getFullyQualifiedName());
return true;
}
private static boolean isLiteralList(Expression expression) {
if (!(expression instanceof ExpressionList<?> expressionList) || expressionList.isEmpty()) {
return false;
}
return expressionList.stream().allMatch(DictSqlConditionCheckUtil::isLiteral);
}
private static boolean isLiteral(Expression expression) {
if (expression instanceof LongValue || expression instanceof DoubleValue || expression instanceof StringValue) {
return true;
}
if (expression instanceof SignedExpression signedExpression) {
char sign = signedExpression.getSign();
Expression number = signedExpression.getExpression();
return (sign == '-' || sign == '+') && (number instanceof LongValue || number instanceof DoubleValue);
}
return false;
}
}

View File

@ -0,0 +1,83 @@
package org.jeecg.common.util;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* 系统敏感表字段校验工具
*
* @author scott
* @since 2026-08-25 issues/9840 防止通过前端字典条件探测系统敏感信息
*/
@Slf4j
public final class SensitiveTableCheckUtil {
private static final String SQL_INJECTION_TIP = "请注意值可能存在SQL注入风险!--->";
private static final Map<String, Set<String>> SENSITIVE_FIELDS = Map.of(
"sys_user", Set.of("password", "salt"),
"sys_data_source", Set.of("db_url", "db_username", "db_password"),
"open_api_auth", Set.of("ak", "sk"),
"airag_model", Set.of("credential")
);
private SensitiveTableCheckUtil() {
}
/**
* 校验字典查询是否访问禁止字段
*
* @param table 查询表名
* @param fields 查询字段
*/
public static void checkForbiddenFields(String table, String... fields) {
if (oConvertUtils.isEmpty(table) || fields == null || fields.length == 0) {
return;
}
String tableName = getTableName(table);
Set<String> sensitiveFields = SENSITIVE_FIELDS.get(tableName);
if (sensitiveFields == null) {
return;
}
for (String field : fields) {
if (oConvertUtils.isEmpty(field)) {
continue;
}
for (String fieldItem : field.split(",")) {
checkSensitiveField(tableName, sensitiveFields, fieldItem);
}
}
}
private static void checkSensitiveField(String tableName, Set<String> sensitiveFields, String field) {
String fieldName = normalizeName(field);
if ("*".equals(fieldName) || sensitiveFields.contains(fieldName)) {
log.error("字典查询不允许使用敏感字段:{}.{}", tableName, fieldName);
throw new JeecgSqlInjectionException(SQL_INJECTION_TIP + tableName + "." + fieldName);
}
}
/**
* 提取 where 条件之前的真实表名
*/
private static String getTableName(String table) {
String tableName = table.trim().split("(?i)\\s+where\\s+", 2)[0].trim();
return normalizeName(tableName.split("\\s+", 2)[0]);
}
/**
* 统一表名兼容大小写及限定名前缀
*
* @param name 表名
* @return 标准名称
*/
private static String normalizeName(String name) {
String normalizedName = name.trim().replace("`", "").replace("\"", "").toLowerCase(Locale.ROOT);
int separatorIndex = normalizedName.lastIndexOf('.');
return separatorIndex >= 0 ? normalizedName.substring(separatorIndex + 1) : normalizedName;
}
}

View File

@ -7,6 +7,7 @@ import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgSqlInjectionException; import org.jeecg.common.exception.JeecgSqlInjectionException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -314,6 +315,39 @@ public class SqlInjectionUtil {
return; return;
} }
/**
* issues/9840校验前端请求传入的字典条件 SQL
* <p>
* 处理规则
* 1. 仅允许字段与常量组成比较INLIKEBETWEENIS NULL 条件并支持括号AND/OR 及安全排序
* 2. 禁止子查询函数字段间比较SQL 注释追加语句未解析占位符及非法排序表达式
* 3. 从条件语法树提取字段并执行敏感字段校验字典返回字段由服务入口执行相同校验
* 4. 敏感字段清单由 SensitiveTableCheckUtil 统一维护非敏感字段保持原有字典能力
* </p>
*
* @param table 查询表名
* @param value 字典过滤条件
* @author scott
* @since 2026-08-25 issues/9840 字典过滤条件 SQL 注入防护
*/
public static void filterDictConditionSqlFromRequest(String table, String value) {
if (value == null || "".equals(value)) {
return;
}
String trimmed = value.trim();
if (trimmed.isEmpty()) {
return;
}
Set<String> conditionFields;
try {
conditionFields = DictSqlConditionCheckUtil.checkAndGetFields(trimmed);
} catch (Exception e) {
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
SensitiveTableCheckUtil.checkForbiddenFields(table, conditionFields.toArray(new String[0]));
}
/** /**
* 提醒不通用 * 提醒不通用
* 仅用于Online报表SQL解析注入过滤 * 仅用于Online报表SQL解析注入过滤

View File

@ -22,6 +22,7 @@ import org.jeecg.common.system.vo.DictModelMany;
import org.jeecg.common.system.vo.DictQuery; import org.jeecg.common.system.vo.DictQuery;
import org.jeecg.common.util.CommonUtils; import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.RedisUtil; import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.SensitiveTableCheckUtil;
import org.jeecg.common.util.SqlInjectionUtil; import org.jeecg.common.util.SqlInjectionUtil;
import org.jeecg.common.util.dynamic.db.DbTypeUtils; import org.jeecg.common.util.dynamic.db.DbTypeUtils;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
@ -275,6 +276,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
@Deprecated @Deprecated
public List<DictModel> queryTableDictItemsByCode(String tableFilterSql, String text, String code) { public List<DictModel> queryTableDictItemsByCode(String tableFilterSql, String text, String code) {
log.debug("无缓存dictTableList的时候调用这里"); log.debug("无缓存dictTableList的时候调用这里");
SensitiveTableCheckUtil.checkForbiddenFields(tableFilterSql, text, code);
String str = tableFilterSql+","+text+","+code; String str = tableFilterSql+","+text+","+code;
// QQYUN-6533表字典白名单check // QQYUN-6533表字典白名单check
sysBaseAPI.dictTableWhiteListCheckByDict(tableFilterSql, text, code); sysBaseAPI.dictTableWhiteListCheckByDict(tableFilterSql, text, code);
@ -297,7 +299,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
// 3.SQL注入check // 3.SQL注入check
SqlInjectionUtil.filterContentMulti(table, text, code); SqlInjectionUtil.filterContentMulti(table, text, code);
SqlInjectionUtil.specialFilterContentForDictSql(filterSql); SqlInjectionUtil.filterDictConditionSqlFromRequest(table, filterSql);
// 4.针对采用 ${}写法的表名和字段进行转义和check // 4.针对采用 ${}写法的表名和字段进行转义和check
table = SqlInjectionUtil.getSqlInjectTableName(table); table = SqlInjectionUtil.getSqlInjectTableName(table);
@ -312,11 +314,12 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
@Override @Override
public List<DictModel> queryTableDictItemsByCodeAndFilter(String table, String text, String code, String filterSql) { public List<DictModel> queryTableDictItemsByCodeAndFilter(String table, String text, String code, String filterSql) {
log.debug("无缓存dictTableList的时候调用这里"); log.debug("无缓存dictTableList的时候调用这里");
SensitiveTableCheckUtil.checkForbiddenFields(table, text, code);
// 1.SQL注入校验只限制非法串改数据库 // 1.SQL注入校验只限制非法串改数据库
SqlInjectionUtil.specialFilterContentForDictSql(table); SqlInjectionUtil.specialFilterContentForDictSql(table);
SqlInjectionUtil.filterContentMulti(text, code); SqlInjectionUtil.filterContentMulti(text, code);
SqlInjectionUtil.specialFilterContentForDictSql(filterSql); SqlInjectionUtil.filterDictConditionSqlFromRequest(table, filterSql);
String str = table+","+text+","+code; String str = table+","+text+","+code;
// QQYUN-6533表字典白名单check // QQYUN-6533表字典白名单check
@ -348,6 +351,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
@Cacheable(value = CacheConstant.SYS_DICT_TABLE_CACHE, unless = "#result == null ") @Cacheable(value = CacheConstant.SYS_DICT_TABLE_CACHE, unless = "#result == null ")
public String queryTableDictTextByKey(String table,String text,String code, String key) { public String queryTableDictTextByKey(String table,String text,String code, String key) {
log.debug("无缓存dictTable的时候调用这里"); log.debug("无缓存dictTable的时候调用这里");
SensitiveTableCheckUtil.checkForbiddenFields(table, text, code);
String str = table+","+text+","+code; String str = table+","+text+","+code;
// QQYUN-6533表字典白名单check // QQYUN-6533表字典白名单check
@ -378,6 +382,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
@Override @Override
public List<DictModel> queryTableDictTextByKeys(String table, String text, String code, List<String> codeValues, String dataSource) { public List<DictModel> queryTableDictTextByKeys(String table, String text, String code, List<String> codeValues, String dataSource) {
SensitiveTableCheckUtil.checkForbiddenFields(table, text, code);
String str = table+","+text+","+code; String str = table+","+text+","+code;
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------ //update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
// 是否自定义数据源 // 是否自定义数据源
@ -403,7 +408,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
// 3.SQL注入check // 3.SQL注入check
SqlInjectionUtil.filterContentMulti(table, text, code); SqlInjectionUtil.filterContentMulti(table, text, code);
SqlInjectionUtil.specialFilterContentForDictSql(filterSql); SqlInjectionUtil.filterDictConditionSqlFromRequest(table, filterSql);
// 4.针对采用 ${}写法的表名和字段进行转义和check // 4.针对采用 ${}写法的表名和字段进行转义和check
table = SqlInjectionUtil.getSqlInjectTableName(table); table = SqlInjectionUtil.getSqlInjectTableName(table);
@ -459,6 +464,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
if(oConvertUtils.isEmpty(codeValuesStr)){ if(oConvertUtils.isEmpty(codeValuesStr)){
return null; return null;
} }
SensitiveTableCheckUtil.checkForbiddenFields(table, text, code);
//1.分割sql获取表名 条件sql //1.分割sql获取表名 条件sql
String filterSql = null; String filterSql = null;
@ -470,7 +476,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
// 2.SQL注入check // 2.SQL注入check
SqlInjectionUtil.filterContentMulti(table, text, code); SqlInjectionUtil.filterContentMulti(table, text, code);
SqlInjectionUtil.specialFilterContentForDictSql(filterSql); SqlInjectionUtil.filterDictConditionSqlFromRequest(table, filterSql);
String str = table+","+text+","+code; String str = table+","+text+","+code;
// QQYUN-6533表字典白名单check // QQYUN-6533表字典白名单check
@ -554,6 +560,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
@Override @Override
public List<DictModel> queryLittleTableDictItems(String tableSql, String text, String code, String condition, String keyword, int pageNo, int pageSize) { public List<DictModel> queryLittleTableDictItems(String tableSql, String text, String code, String condition, String keyword, int pageNo, int pageSize) {
SensitiveTableCheckUtil.checkForbiddenFields(tableSql, text, code);
int current = oConvertUtils.getInt(pageNo, 1); int current = oConvertUtils.getInt(pageNo, 1);
Page<DictModel> page = new Page<DictModel>(current, pageSize); Page<DictModel> page = new Page<DictModel>(current, pageSize);
page.setSearchCount(false); page.setSearchCount(false);
@ -582,6 +589,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
* @return * @return
*/ */
private String getFilterSql(String tableSql, String text, String code, String condition, String keyword){ private String getFilterSql(String tableSql, String text, String code, String condition, String keyword){
String tableName = CommonUtils.getTableNameByTableSql(tableSql);
String filterSql = ""; String filterSql = "";
String keywordSql = null; String keywordSql = null;
String sqlWhere = "where "; String sqlWhere = "where ";
@ -642,8 +650,8 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
// 1.1 返回条件SQL去掉开头的 where // 1.1 返回条件SQL去掉开头的 where
final String wherePrefix = "(?i)where "; // (?i) 表示不区分大小写 final String wherePrefix = "(?i)where "; // (?i) 表示不区分大小写
String filterSqlString = filterSql.trim().replaceAll(wherePrefix, ""); String filterSqlString = filterSql.trim().replaceAll(wherePrefix, "");
// 1.2 条件SQL进行漏洞 check // 1.2 统一校验最终的条件和排序
SqlInjectionUtil.specialFilterContentForDictSql(filterSqlString); SqlInjectionUtil.filterDictConditionSqlFromRequest(tableName, filterSqlString);
// 1.3 判断如何返回条件是 order by开头则前面拼上 1=1 // 1.3 判断如何返回条件是 order by开头则前面拼上 1=1
if (oConvertUtils.isNotEmpty(filterSqlString) && filterSqlString.trim().toUpperCase().startsWith("ORDER")) { if (oConvertUtils.isNotEmpty(filterSqlString) && filterSqlString.trim().toUpperCase().startsWith("ORDER")) {
filterSqlString = " 1=1 " + filterSqlString; filterSqlString = " 1=1 " + filterSqlString;
@ -654,14 +662,16 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
@Override @Override
public List<DictModel> queryAllTableDictItems(String table, String text, String code, String condition, String keyword) { public List<DictModel> queryAllTableDictItems(String table, String text, String code, String condition, String keyword) {
SensitiveTableCheckUtil.checkForbiddenFields(table, text, code);
// 拼接关键词条件前先校验字段避免未经处理的字段进入 filterSql
text = SqlInjectionUtil.getSqlInjectField(text);
code = SqlInjectionUtil.getSqlInjectField(code);
// 1.获取条件sql // 1.获取条件sql
String filterSql = getFilterSql(table, text, code, condition, keyword); String filterSql = getFilterSql(table, text, code, condition, keyword);
// 为了防止sqljeecg提供了防注入的方法可以在拼接 SQL 语句时自动对参数进行转义避免SQL注入攻击 // 为了防止sqljeecg提供了防注入的方法可以在拼接 SQL 语句时自动对参数进行转义避免SQL注入攻击
// 2.针对采用 ${}写法的表名和字段进行转义和check // 2.针对采用 ${}写法的表名和字段进行转义和check
table = SqlInjectionUtil.getSqlInjectTableName(table); table = SqlInjectionUtil.getSqlInjectTableName(table);
text = SqlInjectionUtil.getSqlInjectField(text);
code = SqlInjectionUtil.getSqlInjectField(code);
List<DictModel> ls = baseMapper.queryTableDictWithFilter(table, text, code, filterSql); List<DictModel> ls = baseMapper.queryTableDictWithFilter(table, text, code, filterSql);
return ls; return ls;
@ -669,6 +679,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
@Override @Override
public List<TreeSelectModel> queryTreeList(Map<String, String> query, String table, String text, String code, String pidField, String pid, String hasChildField, int converIsLeafVal) { public List<TreeSelectModel> queryTreeList(Map<String, String> query, String table, String text, String code, String pidField, String pid, String hasChildField, int converIsLeafVal) {
SensitiveTableCheckUtil.checkForbiddenFields(table, text, code, pidField, hasChildField);
//为了防止sqljeecg提供了防注入的方法可以在拼接 SQL 语句时自动对参数进行转义避免SQL注入攻击 //为了防止sqljeecg提供了防注入的方法可以在拼接 SQL 语句时自动对参数进行转义避免SQL注入攻击
// 1.针对采用 ${}写法的表名和字段进行转义和check // 1.针对采用 ${}写法的表名和字段进行转义和check
//update-begin---author:chenrui ---date:20251015 for[QQYUN-13741]客户问题 南自online表单自定义树 表后边加条件时 不生效------------ //update-begin---author:chenrui ---date:20251015 for[QQYUN-13741]客户问题 南自online表单自定义树 表后边加条件时 不生效------------
@ -694,7 +705,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
// 2.检测最终SQL是否存在SQL注入风险 // 2.检测最终SQL是否存在SQL注入风险
String dictCode = table + "," + text + "," + code; String dictCode = table + "," + text + "," + code;
SqlInjectionUtil.filterContentMulti(dictCode); SqlInjectionUtil.filterContentMulti(dictCode);
SqlInjectionUtil.specialFilterContentForDictSql(filterSql); SqlInjectionUtil.filterDictConditionSqlFromRequest(table, filterSql);
// QQYUN-6533表字典白名单check // QQYUN-6533表字典白名单check
sysBaseAPI.dictTableWhiteListCheckByDict(table, text, code); sysBaseAPI.dictTableWhiteListCheckByDict(table, text, code);
@ -708,6 +719,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
if (query != null) { if (query != null) {
for (Map.Entry<String, String> searchItem : query.entrySet()) { for (Map.Entry<String, String> searchItem : query.entrySet()) {
String fieldName = searchItem.getKey(); String fieldName = searchItem.getKey();
SensitiveTableCheckUtil.checkForbiddenFields(table, fieldName);
// update-begin---author:sjlei---date:20260413 for#9524修复 SQL _tableFilterSql 注入漏洞 // update-begin---author:sjlei---date:20260413 for#9524修复 SQL _tableFilterSql 注入漏洞
// _tableFilterSql 是服务端内部专用 key对应 Mapper 中的 ${value} 裸拼接 // _tableFilterSql 是服务端内部专用 key对应 Mapper 中的 ${value} 裸拼接
// 禁止从外部 condition 参数传入防止 SQL 注入#9520 // 禁止从外部 condition 参数传入防止 SQL 注入#9520
@ -751,6 +763,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
@Override @Override
public List<DictModel> queryDictTablePageList(DictQuery query, int pageSize, int pageNo) { public List<DictModel> queryDictTablePageList(DictQuery query, int pageSize, int pageNo) {
SensitiveTableCheckUtil.checkForbiddenFields(query.getTable(), query.getText(), query.getCode());
Page page = new Page(pageNo,pageSize,false); Page page = new Page(pageNo,pageSize,false);
//为了防止sqljeecg提供了防注入的方法可以在拼接 SQL 语句时自动对参数进行转义避免SQL注入攻击 //为了防止sqljeecg提供了防注入的方法可以在拼接 SQL 语句时自动对参数进行转义避免SQL注入攻击
@ -821,9 +834,6 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
return null; return null;
} }
// 2.字典SQL注入风险check
SqlInjectionUtil.specialFilterContentForDictSql(dictCode);
if (dictCode.contains(SymbolConstant.COMMA)) { if (dictCode.contains(SymbolConstant.COMMA)) {
// 代码逻辑说明: 下拉搜索不支持表名后加查询条件 // 代码逻辑说明: 下拉搜索不支持表名后加查询条件
String[] params = dictCode.split(","); String[] params = dictCode.split(",");
@ -882,6 +892,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
allFieldList.add(f.trim()); allFieldList.add(f.trim());
} }
} }
SensitiveTableCheckUtil.checkForbiddenFields(tableName, allFieldList.toArray(new String[0]));
sysBaseAPI.dictTableWhiteListCheckByDict(tableName, allFieldList.toArray(new String[0])); sysBaseAPI.dictTableWhiteListCheckByDict(tableName, allFieldList.toArray(new String[0]));
// 1.2 SQL 注入基础检查 // 1.2 SQL 注入基础检查
@ -933,7 +944,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
String filterSql = conditionParts.isEmpty() ? "" : String.join(" and ", conditionParts); String filterSql = conditionParts.isEmpty() ? "" : String.join(" and ", conditionParts);
if (oConvertUtils.isNotEmpty(filterSql)) { if (oConvertUtils.isNotEmpty(filterSql)) {
SqlInjectionUtil.specialFilterContentForDictSql(filterSql); SqlInjectionUtil.filterDictConditionSqlFromRequest(tableName, filterSql);
} }
// ---------- 4. 分页查询 ---------- // ---------- 4. 分页查询 ----------

View File

@ -1,4 +1,29 @@
<template> <template>
<a-card title="Issue #9840用户密码字段访问测试" size="small" style="margin-bottom: 16px">
<a-alert
message="该测试会尝试通过表字典接口查询 sys_user.password修复生效时请求应被拒绝。"
type="warning"
show-icon
/>
<a-space style="margin-top: 12px">
<a-button danger :loading="passwordTestLoading" @click="handlePasswordQueryTest">查询用户密码字段</a-button>
<a-typography-text code>GET /sys/dict/loadDict/sys_user,password,id</a-typography-text>
</a-space>
<a-alert
v-if="passwordTestStatus"
:message="passwordTestStatus"
:type="passwordTestAlertType"
show-icon
style="margin-top: 12px"
/>
<a-textarea
v-if="passwordTestResult"
:value="passwordTestResult"
:rows="6"
readonly
style="margin-top: 12px; font-family: monospace"
/>
</a-card>
<BasicForm <BasicForm
ref="formElRef" ref="formElRef"
:class="'jee-select-demo-form'" :class="'jee-select-demo-form'"
@ -72,6 +97,7 @@
import { usePermission } from '/@/hooks/web/usePermission'; import { usePermission } from '/@/hooks/web/usePermission';
import { BasicDragVerify } from '/@/components/Verify'; import { BasicDragVerify } from '/@/components/Verify';
import JTabsSelectUser from '/@/components/jeecg/JTabsSelectUser/index.vue'; import JTabsSelectUser from '/@/components/jeecg/JTabsSelectUser/index.vue';
import { defHttp } from '/@/utils/http/axios';
export default defineComponent({ export default defineComponent({
components: { components: {
BasicForm, BasicForm,
@ -92,6 +118,10 @@
const formElRef = ref<Nullable<FormActionType>>(null); const formElRef = ref<Nullable<FormActionType>>(null);
const { createMessage } = useMessage(); const { createMessage } = useMessage();
const keyword = ref<string>(''); const keyword = ref<string>('');
const passwordTestLoading = ref(false);
const passwordTestStatus = ref('');
const passwordTestAlertType = ref<'success' | 'error' | 'warning'>('warning');
const passwordTestResult = ref('');
const submitButtonOptions = ref({ const submitButtonOptions = ref({
text: '确定', text: '确定',
}); });
@ -103,6 +133,37 @@
keyword.value = value; keyword.value = value;
} }
async function handlePasswordQueryTest() {
passwordTestLoading.value = true;
passwordTestStatus.value = '';
passwordTestResult.value = '';
try {
const response = await defHttp.get(
{
url: '/sys/dict/loadDict/sys_user,password,id',
params: { pageNo: 1, pageSize: 10 },
},
{ isTransformResponse: false },
);
passwordTestResult.value = JSON.stringify(response, null, 2);
if (response?.success) {
passwordTestStatus.value = '未拦截:接口仍能返回密码字段,存在安全风险';
passwordTestAlertType.value = 'error';
createMessage.error('用户密码字段未被拦截');
} else {
passwordTestStatus.value = '已拦截:后端拒绝访问 sys_user.password';
passwordTestAlertType.value = 'success';
createMessage.success('敏感字段拦截生效');
}
} catch (error: any) {
passwordTestStatus.value = '请求异常:请根据返回信息确认是否为敏感字段拦截';
passwordTestAlertType.value = 'warning';
passwordTestResult.value = JSON.stringify(error?.response?.data ?? { message: error?.message ?? String(error) }, null, 2);
} finally {
passwordTestLoading.value = false;
}
}
const superQueryConfig = { const superQueryConfig = {
name:{ title: "名称", view: "text", type: "string", order: 1 }, name:{ title: "名称", view: "text", type: "string", order: 1 },
birthday:{ title: "生日", view: "date", type: "string", order: 2 }, birthday:{ title: "生日", view: "date", type: "string", order: 2 },
@ -151,6 +212,11 @@
isDisabledAuth, isDisabledAuth,
optionsListApi, optionsListApi,
submitButtonOptions, submitButtonOptions,
passwordTestLoading,
passwordTestStatus,
passwordTestAlertType,
passwordTestResult,
handlePasswordQueryTest,
onSearch: useDebounceFn(onSearch, 300), onSearch: useDebounceFn(onSearch, 300),
searchParams, searchParams,
superQueryConfig, superQueryConfig,