Enhance security: SQL injection prevention, secret management, and endpoint protection

Co-authored-by: zhijiantianya <zhijiantianya@gmail.com>
This commit is contained in:
Cursor Agent
2025-07-03 02:26:31 +00:00
parent 7cee12c412
commit eb3aeaede2
6 changed files with 340 additions and 40 deletions

View File

@@ -1,17 +1,21 @@
package cn.iocoder.yudao.module.report.service.goview;
import cn.iocoder.yudao.module.report.controller.admin.goview.vo.data.GoViewDataRespVO;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import com.google.common.collect.Maps;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.springframework.jdbc.support.rowset.SqlRowSetMetaData;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import cn.hutool.core.util.StrUtil;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Map;
import java.util.regex.Pattern;
/**
* GoView 数据 Service 实现类
@@ -28,28 +32,93 @@ public class GoViewDataServiceImpl implements GoViewDataService {
@Resource
private JdbcTemplate jdbcTemplate;
// SQL注入防护只允许SELECT查询禁止危险操作
private static final Pattern SQL_INJECTION_PATTERN = Pattern.compile(
"(?i).*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE|SCRIPT|UNION|INFORMATION_SCHEMA|SLEEP|BENCHMARK|WAITFOR).*"
);
// 只允许SELECT查询
private static final Pattern VALID_SELECT_PATTERN = Pattern.compile(
"^\\s*SELECT\\s+.*", Pattern.CASE_INSENSITIVE
);
@Override
public GoViewDataRespVO getDataBySQL(String sql) {
// 1. 执行查询
SqlRowSet sqlRowSet = jdbcTemplate.queryForRowSet(sql);
// 2. 构建返回结果
GoViewDataRespVO respVO = new GoViewDataRespVO();
// 2.1 解析元数据
SqlRowSetMetaData metaData = sqlRowSet.getMetaData();
String[] columnNames = metaData.getColumnNames();
respVO.setDimensions(Arrays.asList(columnNames));
// 2.2 解析数据明细
respVO.setSource(new LinkedList<>()); // 由于数据量不确认,使用 LinkedList 虽然内存占用大一点,但是不存在扩容复制的问题
while (sqlRowSet.next()) {
Map<String, Object> data = Maps.newHashMapWithExpectedSize(columnNames.length);
for (String columnName : columnNames) {
data.put(columnName, sqlRowSet.getObject(columnName));
}
respVO.getSource().add(data);
// 参数校验
if (StrUtil.isBlank(sql)) {
throw ServiceExceptionUtil.exception(new ErrorCode(400, "SQL语句不能为空"));
}
// SQL安全校验
validateSqlSecurity(sql);
// 限制SQL长度防止过长查询
if (sql.length() > 2000) {
throw ServiceExceptionUtil.exception(new ErrorCode(400, "SQL语句长度不能超过2000字符"));
}
try {
// 1. 执行查询
SqlRowSet sqlRowSet = jdbcTemplate.queryForRowSet(sql);
// 2. 构建返回结果
GoViewDataRespVO respVO = new GoViewDataRespVO();
// 2.1 解析元数据
SqlRowSetMetaData metaData = sqlRowSet.getMetaData();
String[] columnNames = metaData.getColumnNames();
respVO.setDimensions(Arrays.asList(columnNames));
// 2.2 解析数据明细
respVO.setSource(new LinkedList<>()); // 由于数据量不确认,使用 LinkedList 虽然内存占用大一点,但是不存在扩容复制的问题
int rowCount = 0;
final int MAX_ROWS = 10000; // 限制返回行数,防止内存溢出
while (sqlRowSet.next() && rowCount < MAX_ROWS) {
Map<String, Object> data = Maps.newHashMapWithExpectedSize(columnNames.length);
for (String columnName : columnNames) {
data.put(columnName, sqlRowSet.getObject(columnName));
}
respVO.getSource().add(data);
rowCount++;
}
return respVO;
} catch (Exception e) {
throw ServiceExceptionUtil.exception(new ErrorCode(500, "SQL执行失败" + e.getMessage()));
}
}
/**
* 验证SQL安全性
* @param sql SQL语句
*/
private void validateSqlSecurity(String sql) {
String normalizedSql = sql.trim().replaceAll("\\s+", " ");
// 1. 检查是否为SELECT语句
if (!VALID_SELECT_PATTERN.matcher(normalizedSql).matches()) {
throw ServiceExceptionUtil.exception(new ErrorCode(403, "只允许执行SELECT查询语句"));
}
// 2. 检查危险操作
if (SQL_INJECTION_PATTERN.matcher(normalizedSql).matches()) {
throw ServiceExceptionUtil.exception(new ErrorCode(403, "SQL语句包含不允许的操作"));
}
// 3. 检查注释和多语句
if (normalizedSql.contains("--") || normalizedSql.contains("/*") || normalizedSql.contains(";")) {
throw ServiceExceptionUtil.exception(new ErrorCode(403, "SQL语句不能包含注释或多条语句"));
}
// 4. 检查系统表访问
String upperSql = normalizedSql.toUpperCase();
if (upperSql.contains("INFORMATION_SCHEMA") ||
upperSql.contains("MYSQL.USER") ||
upperSql.contains("SYS.") ||
upperSql.contains("PERFORMANCE_SCHEMA")) {
throw ServiceExceptionUtil.exception(new ErrorCode(403, "不允许访问系统表"));
}
return respVO;
}
}