fix:【漏洞】 ExportCenter 模块中存在 IDOR(不安全直接对象引用)漏洞

This commit is contained in:
tjlygdx
2026-05-25 18:19:53 +08:00
parent 26c847325c
commit 2e83e2a608
7 changed files with 180 additions and 72 deletions

View File

@@ -1,5 +1,10 @@
package io.dataease.exportCenter.manage;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.Verification;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -7,6 +12,7 @@ import io.dataease.api.chart.request.ChartExcelRequest;
import io.dataease.api.dataset.dto.DataSetExportRequest;
import io.dataease.api.export.BaseExportApi;
import io.dataease.api.xpack.dataFilling.DataFillingApi;
import io.dataease.auth.bo.TokenUserBO;
import io.dataease.commons.utils.ExcelWatermarkUtils;
import io.dataease.constant.LogOT;
import io.dataease.constant.LogST;
@@ -32,6 +38,7 @@ import io.dataease.visualization.server.DataVisualizationServer;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Data;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.springframework.beans.factory.annotation.Autowired;
@@ -39,9 +46,11 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ReflectionUtils;
import io.dataease.visualization.dto.WatermarkContentDTO;
import io.dataease.api.permissions.user.vo.UserFormVO;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.Future;
@@ -88,53 +97,28 @@ public class ExportCenterManage implements BaseExportApi {
return "sync";
}
public void download(String id, HttpServletResponse response) throws Exception {
if (coreExportDownloadTaskMapper.selectById(id) == null) {
DEException.throwException("任务不存在");
}
CoreExportTask exportTask = exportTaskMapper.selectById(id);
public void download(String id, String ticket, HttpServletResponse response) throws Exception {
CoreExportTask exportTask = validateDownloadTask(id, ticket);
exportCenterDownLoadManage.download(exportTask, response);
}
public void delete(String id) {
QueryWrapper<CoreExportTask> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("id", id);
if (exportTaskMapper.exists(queryWrapper)) {
Iterator<Map.Entry<String, Future>> iterator = Running_Task.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Future> entry = iterator.next();
if (entry.getKey().equalsIgnoreCase(id)) {
entry.getValue().cancel(true);
iterator.remove();
}
}
FileUtils.deleteDirectoryRecursively(exportData_path + id);
exportTaskMapper.deleteById(id);
}
CoreExportTask exportTask = getCurrentUserExportTask(id);
deleteTask(exportTask);
}
public void deleteAll(String type) {
if (!STATUS.contains(type)) {
DEException.throwException("无效的状态");
}
Long currentUserId = currentUserId();
QueryWrapper<CoreExportTask> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", AuthUtils.getUser().getUserId());
queryWrapper.eq("user_id", currentUserId);
if (!type.equalsIgnoreCase("ALL")) {
queryWrapper.eq("export_status", type);
}
List<CoreExportTask> exportTasks = exportTaskMapper.selectList(queryWrapper);
exportTasks.parallelStream().forEach(exportTask -> {
Iterator<Map.Entry<String, Future>> iterator = Running_Task.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Future> entry = iterator.next();
if (entry.getKey().equalsIgnoreCase(exportTask.getId())) {
entry.getValue().cancel(true);
iterator.remove();
}
}
FileUtils.deleteDirectoryRecursively(exportData_path + exportTask.getId());
exportTaskMapper.deleteById(exportTask.getId());
});
exportTasks.parallelStream().forEach(this::deleteTask);
}
@@ -143,7 +127,7 @@ public class ExportCenterManage implements BaseExportApi {
}
public void retry(String id) {
CoreExportTask exportTask = exportTaskMapper.selectById(id);
CoreExportTask exportTask = getCurrentUserExportTask(id);
if (!exportTask.getExportStatus().equalsIgnoreCase("FAILED")) {
DEException.throwException("正在导出中!");
}
@@ -172,8 +156,9 @@ public class ExportCenterManage implements BaseExportApi {
DEException.throwException("Invalid status: " + status);
}
Long currentUserId = currentUserId();
QueryWrapper<CoreExportTask> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", AuthUtils.getUser().getUserId());
queryWrapper.eq("user_id", currentUserId);
if (!status.equalsIgnoreCase("ALL")) {
queryWrapper.eq("export_status", status);
}
@@ -194,29 +179,30 @@ public class ExportCenterManage implements BaseExportApi {
}
public Map<String, Long> exportTasks() {
Long currentUserId = currentUserId();
Map<String, Long> result = new HashMap<>();
QueryWrapper<CoreExportTask> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", AuthUtils.getUser().getUserId());
queryWrapper.eq("user_id", currentUserId);
queryWrapper.eq("export_status", "IN_PROGRESS");
result.put("IN_PROGRESS", exportTaskMapper.selectCount(queryWrapper));
queryWrapper.clear();
queryWrapper.eq("user_id", AuthUtils.getUser().getUserId());
queryWrapper.eq("user_id", currentUserId);
queryWrapper.eq("export_status", "SUCCESS");
result.put("SUCCESS", exportTaskMapper.selectCount(queryWrapper));
queryWrapper.clear();
queryWrapper.eq("user_id", AuthUtils.getUser().getUserId());
queryWrapper.eq("user_id", currentUserId);
queryWrapper.eq("export_status", "FAILED");
result.put("FAILED", exportTaskMapper.selectCount(queryWrapper));
queryWrapper.clear();
queryWrapper.eq("user_id", AuthUtils.getUser().getUserId());
queryWrapper.eq("user_id", currentUserId);
queryWrapper.eq("export_status", "PENDING");
result.put("PENDING", exportTaskMapper.selectCount(queryWrapper));
queryWrapper.clear();
queryWrapper.eq("user_id", AuthUtils.getUser().getUserId());
queryWrapper.eq("user_id", currentUserId);
result.put("ALL", exportTaskMapper.selectCount(queryWrapper));
return result;
}
@@ -261,9 +247,10 @@ public class ExportCenterManage implements BaseExportApi {
}
public void addTask(String exportFrom, String exportFromType, ChartExcelRequest request, String busiFlag) {
Long currentUserId = currentUserId();
CoreExportTask exportTask = new CoreExportTask();
exportTask.setId(IDUtils.snowID().toString());
exportTask.setUserId(AuthUtils.getUser().getUserId());
exportTask.setUserId(currentUserId);
exportTask.setExportFrom(Long.valueOf(exportFrom));
exportTask.setExportFromType(exportFromType);
exportTask.setExportStatus("PENDING");
@@ -283,9 +270,10 @@ public class ExportCenterManage implements BaseExportApi {
public void addTask(Long exportFrom, String exportFromType, DataSetExportRequest request) throws Exception {
datasetGroupManage.getDatasetGroupInfoDTO(exportFrom, null);
Long currentUserId = currentUserId();
CoreExportTask exportTask = new CoreExportTask();
exportTask.setId(IDUtils.snowID().toString());
exportTask.setUserId(AuthUtils.getUser().getUserId());
exportTask.setUserId(currentUserId);
exportTask.setExportFrom(exportFrom);
exportTask.setExportFromType(exportFromType);
exportTask.setExportStatus("PENDING");
@@ -329,7 +317,7 @@ public class ExportCenterManage implements BaseExportApi {
long threshold = System.currentTimeMillis() - expTime;
queryWrapper.lt("export_time", threshold);
exportTaskMapper.selectList(queryWrapper).forEach(coreExportTask -> {
delete(coreExportTask.getId());
deleteTask(coreExportTask);
});
}
@@ -338,7 +326,7 @@ public class ExportCenterManage implements BaseExportApi {
VisualizationWatermark watermark = watermarkMapper.selectById("system_default");
WatermarkContentDTO watermarkContent = JsonUtil.parseObject(watermark.getSettingContent(), WatermarkContentDTO.class);
if (watermarkContent.getEnable() && watermarkContent.getExcelEnable()) {
UserFormVO userInfo = visualizationMapper.queryInnerUserInfo(AuthUtils.getUser().getUserId());
UserFormVO userInfo = visualizationMapper.queryInnerUserInfo(currentUserId());
// 在主逻辑中添加水印
int watermarkPictureIdx = ExcelWatermarkUtils.addWatermarkImage(wb, watermarkContent, userInfo); // 生成水印图片并获取 ID
for (Sheet sheet : wb) {
@@ -348,30 +336,80 @@ public class ExportCenterManage implements BaseExportApi {
}
@DeLog(id = "#p0", ot = LogOT.DOWNLOAD, st = LogST.DATA)
public void generateDownloadUri(String id) {
public String generateDownloadUri(String id) {
CoreExportTask exportTask = getCurrentUserExportTask(id);
long createTime = System.currentTimeMillis();
CoreExportDownloadTask coreExportDownloadTask = coreExportDownloadTaskMapper.selectById(id);
if (coreExportDownloadTask != null) {
coreExportDownloadTask.setCreateTime(System.currentTimeMillis());
coreExportDownloadTask.setCreateTime(createTime);
coreExportDownloadTaskMapper.updateById(coreExportDownloadTask);
} else {
coreExportDownloadTask = new CoreExportDownloadTask();
coreExportDownloadTask.setId(id);
coreExportDownloadTask.setCreateTime(System.currentTimeMillis());
coreExportDownloadTask.setCreateTime(createTime);
coreExportDownloadTask.setValidTime(5L);
coreExportDownloadTaskMapper.insert(coreExportDownloadTask);
}
return "/exportCenter/download/" + id + "?ticket=" + buildDownloadTicket(exportTask, createTime, coreExportDownloadTask.getValidTime());
}
private CoreExportTask getCurrentUserExportTask(String id) {
Long currentUserId = currentUserId();
CoreExportTask exportTask = exportTaskMapper.selectById(id);
if (exportTask == null || !Objects.equals(exportTask.getUserId(), currentUserId)) {
DEException.throwException("任务不存在");
}
return exportTask;
}
public void validateDownloadTask(String id) {
CoreExportDownloadTask coreExportDownloadTask = coreExportDownloadTaskMapper.selectById(id);
if (coreExportDownloadTask != null) {
if (System.currentTimeMillis() - coreExportDownloadTask.getCreateTime() <= coreExportDownloadTask.getValidTime() * 60 * 1000) {
DEException.throwException(Translator.get("i18n_download_link_invalid"));
private void deleteTask(CoreExportTask exportTask) {
if (exportTask == null) {
return;
}
String id = exportTask.getId();
Iterator<Map.Entry<String, Future>> iterator = Running_Task.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Future> entry = iterator.next();
if (entry.getKey().equalsIgnoreCase(id)) {
entry.getValue().cancel(true);
iterator.remove();
}
} else {
}
FileUtils.deleteDirectoryRecursively(exportData_path + id);
exportTaskMapper.deleteById(id);
}
public CoreExportTask validateDownloadTask(String id, String ticket) {
if (StringUtils.isBlank(ticket)) {
DEException.throwException(Translator.get("i18n_download_link_invalid"));
}
CoreExportDownloadTask coreExportDownloadTask = coreExportDownloadTaskMapper.selectById(id);
if (coreExportDownloadTask == null) {
DEException.throwException(Translator.get("i18n_download_link_invalid"));
}
CoreExportTask exportTask = exportTaskMapper.selectById(id);
if (exportTask == null) {
DEException.throwException(Translator.get("i18n_download_link_invalid"));
}
try {
Algorithm algorithm = Algorithm.HMAC256(resolveTicketSecret(exportTask.getUserId()));
Verification verification = JWT.require(algorithm);
JWTVerifier verifier = verification.build();
DecodedJWT jwt = verifier.verify(ticket);
String taskId = jwt.getClaim("taskId").asString();
Long uid = jwt.getClaim("uid").asLong();
Long ticketTime = jwt.getClaim("ts").asLong();
if (!StringUtils.equals(id, taskId)
|| !Objects.equals(uid, exportTask.getUserId())
|| !Objects.equals(ticketTime, coreExportDownloadTask.getCreateTime())
|| System.currentTimeMillis() - coreExportDownloadTask.getCreateTime() > coreExportDownloadTask.getValidTime() * 60 * 1000) {
DEException.throwException(Translator.get("i18n_download_link_invalid"));
}
} catch (Exception e) {
DEException.throwException(Translator.get("i18n_download_link_invalid"));
}
coreExportDownloadTaskMapper.deleteById(id);
return exportTask;
}
@Scheduled(fixedRate = 60 * 60 * 1000)
@@ -389,4 +427,60 @@ public class ExportCenterManage implements BaseExportApi {
Long validTime; // 单位minutes
Long createTime;
}
private String buildDownloadTicket(CoreExportTask exportTask, long createTime, Long validTime) {
Algorithm algorithm = Algorithm.HMAC256(resolveTicketSecret(exportTask.getUserId()));
return JWT.create()
.withClaim("taskId", exportTask.getId())
.withClaim("uid", exportTask.getUserId())
.withClaim("ts", createTime)
.withExpiresAt(new Date(createTime + validTime * 60 * 1000))
.sign(algorithm);
}
private String resolveTicketSecret(Long userId) {
String secret = null;
if (ObjectUtils.isEmpty(CommonBeanFactory.getBean("loginServer"))) {
secret = io.dataease.auth.config.SubstituleLoginConfig.getTokenSecret();
} else {
Object apisixCacheManage = CommonBeanFactory.getBean("apisixCacheManage");
Method userCacheMethod = DeReflectUtil.findMethod(apisixCacheManage.getClass(), "userCacheBO");
Object cacheBO = ReflectionUtils.invokeMethod(userCacheMethod, apisixCacheManage, userId);
Method secretMethod = DeReflectUtil.findMethod(cacheBO.getClass(), "getSecret");
Object secretObj = ReflectionUtils.invokeMethod(secretMethod, cacheBO);
if (secretObj != null) {
secret = secretObj.toString();
}
}
if (StringUtils.isBlank(secret)) {
DEException.throwException(Translator.get("i18n_download_link_invalid"));
}
return secret;
}
private Long currentUserId() {
TokenUserBO user = AuthUtils.getUser();
if (user != null && user.getUserId() != null) {
return user.getUserId();
}
String embeddedToken = ServletUtils.getHead(io.dataease.constant.AuthConstant.EMBEDDED_TOKEN_KEY);
if (StringUtils.isBlank(embeddedToken)) {
DEException.throwException("user not found");
}
Object apisixTokenManage = CommonBeanFactory.getBean("apisixTokenManage");
if (apisixTokenManage == null) {
DEException.throwException("user not found");
}
Method validateEmbeddedTokenMethod = ReflectionUtils.findMethod(apisixTokenManage.getClass(), "validateEmbeddedToken", String.class);
Object tokenBO = ReflectionUtils.invokeMethod(validateEmbeddedTokenMethod, apisixTokenManage, embeddedToken);
if (tokenBO == null) {
DEException.throwException("user not found");
}
Method getUserIdMethod = DeReflectUtil.findMethod(tokenBO.getClass(), "getUserId");
Object userId = ReflectionUtils.invokeMethod(getUserIdMethod, tokenBO);
if (!(userId instanceof Long)) {
DEException.throwException("user not found");
}
return (Long) userId;
}
}

View File

@@ -49,14 +49,13 @@ public class ExportCenterServer implements ExportCenterApi {
}
@Override
public void download(String id, HttpServletResponse response) throws Exception {
exportCenterManage.download(id, response);
public void download(String id, String ticket, HttpServletResponse response) throws Exception {
exportCenterManage.download(id, ticket, response);
}
@Override
public String generateDownloadUri(String id) throws Exception {
exportCenterManage.generateDownloadUri(id);
return "";
return exportCenterManage.generateDownloadUri(id);
}
@Override

View File

@@ -4,8 +4,5 @@
// Generated by unplugin-auto-import
export {}
declare global {
const ElForm: typeof import('element-plus-secondary/es')['ElForm']
const ElFormItem: typeof import('element-plus-secondary/es')['ElFormItem']
const ElInput: typeof import('element-plus-secondary/es')['ElInput']
const ElMessageBox: typeof import('element-plus-secondary/es')['ElMessageBox']
}

View File

@@ -351,10 +351,12 @@ export const exportRetry = async (id): Promise<IResponse> => {
})
}
export const downloadFile = async (id): Promise<Blob> => {
return request.get({ url: 'exportCenter/download/' + id, responseType: 'blob' }).then(res => {
return res?.data
})
export const downloadFile = async (id, ticket): Promise<Blob> => {
return request
.get({ url: 'exportCenter/download/' + id, params: { ticket }, responseType: 'blob' })
.then(res => {
return res?.data
})
}
export const exportDelete = async (id): Promise<IResponse> => {
@@ -363,7 +365,7 @@ export const exportDelete = async (id): Promise<IResponse> => {
})
}
export const generateDownloadUri = async (id): Promise<IResponse> => {
export const generateDownloadUri = async (id): Promise<string> => {
return request.get({ url: '/exportCenter/generateDownloadUri/' + id }).then(res => {
return res?.data
})

View File

@@ -160,15 +160,15 @@ const isDataEaseBi = computed(() => appStore.getIsDataEaseBi)
const downLoadAll = () => {
if (multipleSelection.value.length === 0) {
tableData.value.forEach(item => {
generateDownloadUri(item.id).then(() => {
window.open(PATH_URL + '/exportCenter/download/' + item.id)
generateDownloadUri(item.id).then(uri => {
window.open(PATH_URL + uri)
})
})
return
}
multipleSelection.value.map(ele => {
generateDownloadUri(ele.id).then(() => {
window.open(PATH_URL + '/exportCenter/download/' + ele.id)
generateDownloadUri(ele.id).then(uri => {
window.open(PATH_URL + uri)
})
})
}
@@ -186,8 +186,8 @@ const timestampFormatDate = value => {
import { PATH_URL } from '@/config/axios/service'
import GridTable from '../../../../components/grid-table/src/GridTable.vue'
const downloadClick = item => {
generateDownloadUri(item.id).then(() => {
window.open(PATH_URL + '/exportCenter/download/' + item.id, openType)
generateDownloadUri(item.id).then(uri => {
window.open(PATH_URL + uri, openType)
})
}

View File

@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
import java.util.Map;
@@ -44,7 +45,7 @@ public interface ExportCenterApi {
@Operation(summary = "下载")
@GetMapping("/download/{id}")
public void download(@PathVariable String id, HttpServletResponse response) throws Exception;
public void download(@PathVariable String id, @RequestParam(required = false) String ticket, HttpServletResponse response) throws Exception;
@Operation(summary = "生成下载Url")
@GetMapping("/generateDownloadUri/{id}")

View File

@@ -106,6 +106,21 @@ public class TokenFilter implements Filter {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String embeddedToken = ServletUtils.getHead(AuthConstant.EMBEDDED_TOKEN_KEY);
if (StringUtils.isNotBlank(embeddedToken)) {
Object apisixTokenManage = CommonBeanFactory.getBean("apisixTokenManage");
if (apisixTokenManage == null) {
DEException.throwException("embedded token is invalid");
}
Method validateEmbeddedTokenMethod = ReflectionUtils.findMethod(apisixTokenManage.getClass(), "validateEmbeddedToken", String.class);
Object embeddedUser = ReflectionUtils.invokeMethod(validateEmbeddedTokenMethod, apisixTokenManage, embeddedToken);
if (!(embeddedUser instanceof TokenUserBO)) {
DEException.throwException("embedded token is invalid");
}
UserUtils.setUserInfo((TokenUserBO) embeddedUser);
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String token = ServletUtils.getToken();
TokenUserBO userBO = TokenUtils.validate(token);
UserUtils.setUserInfo(userBO);