mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-08-27 07:08:34 +00:00
解决【issues/9840】临时加固表字典敏感字段访问风险
1. 禁止字典接口查询或通过条件、排序使用敏感字段。 2. 当前限制用户密码、数据源账号密码、OpenAPI密钥及AI模型凭据。 3. 字典条件增加结构化校验,拦截子查询、函数及附加SQL。 4. 增加前端敏感字段访问测试示例。 说明:本次为临时方案,通过敏感表和敏感字段清单进行拦截,暂未调整现有动态SQL架构
This commit is contained in:
parent
898cd0c309
commit
f271cf3420
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -314,6 +315,39 @@ public class SqlInjectionUtil {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 【issues/9840】校验前端请求传入的字典条件 SQL。
|
||||
* <p>
|
||||
* 处理规则:
|
||||
* 1. 仅允许字段与常量组成比较、IN、LIKE、BETWEEN、IS 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解析,注入过滤
|
||||
|
||||
@ -22,6 +22,7 @@ import org.jeecg.common.system.vo.DictModelMany;
|
||||
import org.jeecg.common.system.vo.DictQuery;
|
||||
import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.common.util.SensitiveTableCheckUtil;
|
||||
import org.jeecg.common.util.SqlInjectionUtil;
|
||||
import org.jeecg.common.util.dynamic.db.DbTypeUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
@ -275,6 +276,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
@Deprecated
|
||||
public List<DictModel> queryTableDictItemsByCode(String tableFilterSql, String text, String code) {
|
||||
log.debug("无缓存dictTableList的时候调用这里!");
|
||||
SensitiveTableCheckUtil.checkForbiddenFields(tableFilterSql, text, code);
|
||||
String str = tableFilterSql+","+text+","+code;
|
||||
// 【QQYUN-6533】表字典白名单check
|
||||
sysBaseAPI.dictTableWhiteListCheckByDict(tableFilterSql, text, code);
|
||||
@ -297,7 +299,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
|
||||
// 3.SQL注入check
|
||||
SqlInjectionUtil.filterContentMulti(table, text, code);
|
||||
SqlInjectionUtil.specialFilterContentForDictSql(filterSql);
|
||||
SqlInjectionUtil.filterDictConditionSqlFromRequest(table, filterSql);
|
||||
|
||||
// 4.针对采用 ${}写法的表名和字段进行转义和check
|
||||
table = SqlInjectionUtil.getSqlInjectTableName(table);
|
||||
@ -312,11 +314,12 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
@Override
|
||||
public List<DictModel> queryTableDictItemsByCodeAndFilter(String table, String text, String code, String filterSql) {
|
||||
log.debug("无缓存dictTableList的时候调用这里!");
|
||||
SensitiveTableCheckUtil.checkForbiddenFields(table, text, code);
|
||||
|
||||
// 1.SQL注入校验(只限制非法串改数据库)
|
||||
SqlInjectionUtil.specialFilterContentForDictSql(table);
|
||||
SqlInjectionUtil.filterContentMulti(text, code);
|
||||
SqlInjectionUtil.specialFilterContentForDictSql(filterSql);
|
||||
SqlInjectionUtil.filterDictConditionSqlFromRequest(table, filterSql);
|
||||
|
||||
String str = table+","+text+","+code;
|
||||
// 【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 ")
|
||||
public String queryTableDictTextByKey(String table,String text,String code, String key) {
|
||||
log.debug("无缓存dictTable的时候调用这里!");
|
||||
SensitiveTableCheckUtil.checkForbiddenFields(table, text, code);
|
||||
|
||||
String str = table+","+text+","+code;
|
||||
// 【QQYUN-6533】表字典白名单check
|
||||
@ -378,6 +382,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
|
||||
@Override
|
||||
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;
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
// 是否自定义数据源
|
||||
@ -403,7 +408,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
|
||||
// 3.SQL注入check
|
||||
SqlInjectionUtil.filterContentMulti(table, text, code);
|
||||
SqlInjectionUtil.specialFilterContentForDictSql(filterSql);
|
||||
SqlInjectionUtil.filterDictConditionSqlFromRequest(table, filterSql);
|
||||
|
||||
// 4.针对采用 ${}写法的表名和字段进行转义和check
|
||||
table = SqlInjectionUtil.getSqlInjectTableName(table);
|
||||
@ -459,6 +464,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
if(oConvertUtils.isEmpty(codeValuesStr)){
|
||||
return null;
|
||||
}
|
||||
SensitiveTableCheckUtil.checkForbiddenFields(table, text, code);
|
||||
|
||||
//1.分割sql获取表名 和 条件sql
|
||||
String filterSql = null;
|
||||
@ -470,7 +476,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
|
||||
// 2.SQL注入check
|
||||
SqlInjectionUtil.filterContentMulti(table, text, code);
|
||||
SqlInjectionUtil.specialFilterContentForDictSql(filterSql);
|
||||
SqlInjectionUtil.filterDictConditionSqlFromRequest(table, filterSql);
|
||||
|
||||
String str = table+","+text+","+code;
|
||||
// 【QQYUN-6533】表字典白名单check
|
||||
@ -554,6 +560,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
|
||||
@Override
|
||||
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);
|
||||
Page<DictModel> page = new Page<DictModel>(current, pageSize);
|
||||
page.setSearchCount(false);
|
||||
@ -582,6 +589,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
* @return
|
||||
*/
|
||||
private String getFilterSql(String tableSql, String text, String code, String condition, String keyword){
|
||||
String tableName = CommonUtils.getTableNameByTableSql(tableSql);
|
||||
String filterSql = "";
|
||||
String keywordSql = null;
|
||||
String sqlWhere = "where ";
|
||||
@ -642,8 +650,8 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
// 1.1 返回条件SQL(去掉开头的 where )
|
||||
final String wherePrefix = "(?i)where "; // (?i) 表示不区分大小写
|
||||
String filterSqlString = filterSql.trim().replaceAll(wherePrefix, "");
|
||||
// 1.2 条件SQL进行漏洞 check
|
||||
SqlInjectionUtil.specialFilterContentForDictSql(filterSqlString);
|
||||
// 1.2 统一校验最终的条件和排序
|
||||
SqlInjectionUtil.filterDictConditionSqlFromRequest(tableName, filterSqlString);
|
||||
// 1.3 判断如何返回条件是 order by开头则前面拼上 1=1
|
||||
if (oConvertUtils.isNotEmpty(filterSqlString) && filterSqlString.trim().toUpperCase().startsWith("ORDER")) {
|
||||
filterSqlString = " 1=1 " + filterSqlString;
|
||||
@ -654,14 +662,16 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
|
||||
@Override
|
||||
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
|
||||
String filterSql = getFilterSql(table, text, code, condition, keyword);
|
||||
|
||||
// 为了防止sql(jeecg提供了防注入的方法,可以在拼接 SQL 语句时自动对参数进行转义,避免SQL注入攻击)
|
||||
// 2.针对采用 ${}写法的表名和字段进行转义和check
|
||||
table = SqlInjectionUtil.getSqlInjectTableName(table);
|
||||
text = SqlInjectionUtil.getSqlInjectField(text);
|
||||
code = SqlInjectionUtil.getSqlInjectField(code);
|
||||
|
||||
List<DictModel> ls = baseMapper.queryTableDictWithFilter(table, text, code, filterSql);
|
||||
return ls;
|
||||
@ -669,6 +679,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
|
||||
@Override
|
||||
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);
|
||||
//为了防止sql(jeecg提供了防注入的方法,可以在拼接 SQL 语句时自动对参数进行转义,避免SQL注入攻击)
|
||||
// 1.针对采用 ${}写法的表名和字段进行转义和check
|
||||
//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注入风险
|
||||
String dictCode = table + "," + text + "," + code;
|
||||
SqlInjectionUtil.filterContentMulti(dictCode);
|
||||
SqlInjectionUtil.specialFilterContentForDictSql(filterSql);
|
||||
SqlInjectionUtil.filterDictConditionSqlFromRequest(table, filterSql);
|
||||
|
||||
// 【QQYUN-6533】表字典白名单check
|
||||
sysBaseAPI.dictTableWhiteListCheckByDict(table, text, code);
|
||||
@ -708,6 +719,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
if (query != null) {
|
||||
for (Map.Entry<String, String> searchItem : query.entrySet()) {
|
||||
String fieldName = searchItem.getKey();
|
||||
SensitiveTableCheckUtil.checkForbiddenFields(table, fieldName);
|
||||
// update-begin---author:sjlei---date:20260413 for:【#9524】修复 SQL _tableFilterSql 注入漏洞
|
||||
// _tableFilterSql 是服务端内部专用 key,对应 Mapper 中的 ${value} 裸拼接,
|
||||
// 禁止从外部 condition 参数传入,防止 SQL 注入(#9520)
|
||||
@ -751,6 +763,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
|
||||
@Override
|
||||
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);
|
||||
|
||||
//为了防止sql(jeecg提供了防注入的方法,可以在拼接 SQL 语句时自动对参数进行转义,避免SQL注入攻击)
|
||||
@ -821,9 +834,6 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2.字典SQL注入风险check
|
||||
SqlInjectionUtil.specialFilterContentForDictSql(dictCode);
|
||||
|
||||
if (dictCode.contains(SymbolConstant.COMMA)) {
|
||||
// 代码逻辑说明: 下拉搜索不支持表名后加查询条件
|
||||
String[] params = dictCode.split(",");
|
||||
@ -882,6 +892,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
allFieldList.add(f.trim());
|
||||
}
|
||||
}
|
||||
SensitiveTableCheckUtil.checkForbiddenFields(tableName, allFieldList.toArray(new String[0]));
|
||||
sysBaseAPI.dictTableWhiteListCheckByDict(tableName, allFieldList.toArray(new String[0]));
|
||||
|
||||
// 1.2 SQL 注入基础检查
|
||||
@ -933,7 +944,7 @@ public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDict> impl
|
||||
|
||||
String filterSql = conditionParts.isEmpty() ? "" : String.join(" and ", conditionParts);
|
||||
if (oConvertUtils.isNotEmpty(filterSql)) {
|
||||
SqlInjectionUtil.specialFilterContentForDictSql(filterSql);
|
||||
SqlInjectionUtil.filterDictConditionSqlFromRequest(tableName, filterSql);
|
||||
}
|
||||
|
||||
// ---------- 4. 分页查询 ----------
|
||||
|
||||
@ -1,4 +1,29 @@
|
||||
<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
|
||||
ref="formElRef"
|
||||
:class="'jee-select-demo-form'"
|
||||
@ -72,6 +97,7 @@
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
import { BasicDragVerify } from '/@/components/Verify';
|
||||
import JTabsSelectUser from '/@/components/jeecg/JTabsSelectUser/index.vue';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasicForm,
|
||||
@ -92,6 +118,10 @@
|
||||
const formElRef = ref<Nullable<FormActionType>>(null);
|
||||
const { createMessage } = useMessage();
|
||||
const keyword = ref<string>('');
|
||||
const passwordTestLoading = ref(false);
|
||||
const passwordTestStatus = ref('');
|
||||
const passwordTestAlertType = ref<'success' | 'error' | 'warning'>('warning');
|
||||
const passwordTestResult = ref('');
|
||||
const submitButtonOptions = ref({
|
||||
text: '确定',
|
||||
});
|
||||
@ -102,6 +132,37 @@
|
||||
function onSearch(value: string) {
|
||||
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 = {
|
||||
name:{ title: "名称", view: "text", type: "string", order: 1 },
|
||||
@ -151,6 +212,11 @@
|
||||
isDisabledAuth,
|
||||
optionsListApi,
|
||||
submitButtonOptions,
|
||||
passwordTestLoading,
|
||||
passwordTestStatus,
|
||||
passwordTestAlertType,
|
||||
passwordTestResult,
|
||||
handlePasswordQueryTest,
|
||||
onSearch: useDebounceFn(onSearch, 300),
|
||||
searchParams,
|
||||
superQueryConfig,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user