mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-08-27 15:18:35 +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 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. 仅允许字段与常量组成比较、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解析,注入过滤
|
* 仅用于Online报表SQL解析,注入过滤
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
// 为了防止sql(jeecg提供了防注入的方法,可以在拼接 SQL 语句时自动对参数进行转义,避免SQL注入攻击)
|
// 为了防止sql(jeecg提供了防注入的方法,可以在拼接 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);
|
||||||
//为了防止sql(jeecg提供了防注入的方法,可以在拼接 SQL 语句时自动对参数进行转义,避免SQL注入攻击)
|
//为了防止sql(jeecg提供了防注入的方法,可以在拼接 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);
|
||||||
|
|
||||||
//为了防止sql(jeecg提供了防注入的方法,可以在拼接 SQL 语句时自动对参数进行转义,避免SQL注入攻击)
|
//为了防止sql(jeecg提供了防注入的方法,可以在拼接 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. 分页查询 ----------
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user