fix(upms): 本地文件存储增加路径穿越防护

上传/读取/删除前校验路径位于存储根目录内,统一错误文案,并修复无扩展名时多余的点号。
This commit is contained in:
PIGCLOUD committed 2026-08-03 08:27:00 +08:00
1 parent 6248d60e62
commit d1876914d2
4 files changed
+77 -2

No files matched your search

@@ -74,6 +74,11 @@ public interface UpmsErrorCodes {
String SYS_DICT_ITEM_SORT_SCOPE_INVALID = "sys.dict.item.sort.scopeInvalid";
// === 文件相关 ===
String SYS_FILE_PATH_INVALID = "sys.file.path.invalid";
String SYS_FILE_UPLOAD_FAILED = "sys.file.upload.failed";
// === 参数相关 ===
String SYS_PARAM_CONFIG_ERROR = "sys.param.config.error";
@@ -42,6 +42,10 @@ sys.dict.notFound=Dictionary does not exist
sys.dict.item.sort.duplicate=Dictionary item ID list cannot contain duplicates
sys.dict.item.sort.scopeInvalid=Dictionary item sort data does not belong to the current dictionary
# File related
sys.file.path.invalid=Invalid file path
sys.file.upload.failed=File upload failed
# Parameter related
sys.param.config.error=System parameter configuration error: {0}
sys.param.delete.system=Built-in system parameters cannot be deleted
@@ -42,6 +42,10 @@ sys.dict.notFound=\u5B57\u5178\u4E0D\u5B58\u5728
sys.dict.item.sort.duplicate=\u5B57\u5178\u9879ID\u5217\u8868\u4E0D\u80FD\u91CD\u590D
sys.dict.item.sort.scopeInvalid=\u5B57\u5178\u9879\u6392\u5E8F\u6570\u636E\u4E0D\u5C5E\u4E8E\u5F53\u524D\u5B57\u5178
# 文件相关
sys.file.path.invalid=文件路径不合法
sys.file.upload.failed=文件上传失败
# 参数相关
sys.param.config.error={0} \u7CFB\u7EDF\u53C2\u6570\u914D\u7F6E\u9519\u8BEF
sys.param.delete.system=\u7CFB\u7EDF\u5185\u7F6E\u53C2\u6570\u4E0D\u80FD\u5220\u9664
@@ -30,6 +30,7 @@ import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.HttpUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.admin.api.constant.UpmsErrorCodes;
import com.pig4cloud.pigx.admin.api.dto.SysFileGroupDTO;
import com.pig4cloud.pigx.admin.api.entity.SysFile;
import com.pig4cloud.pigx.admin.api.entity.SysFileGroup;
@@ -37,11 +38,13 @@ import com.pig4cloud.pigx.admin.mapper.SysFileGroupMapper;
import com.pig4cloud.pigx.admin.mapper.SysFileMapper;
import com.pig4cloud.pigx.admin.service.SysFileService;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
import com.pig4cloud.pigx.common.file.core.FileObject;
import com.pig4cloud.pigx.common.file.core.FileProperties;
import com.pig4cloud.pigx.common.file.core.FileTemplate;
import com.pig4cloud.pigx.common.file.core.FileType;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
@@ -51,6 +54,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.*;
@@ -86,8 +90,17 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> impl
public R uploadFile(MultipartFile file, String fileName, String dir, Long groupId, String type) {
// 如果客户端没有传入文件名,则使用UUID生成一个唯一的文件名
if (StrUtil.isBlank(fileName)) {
fileName = IdUtil.simpleUUID() + StrUtil.DOT + FileUtil.extName(file.getOriginalFilename());
String extension = FileUtil.extName(file.getOriginalFilename());
fileName = IdUtil.simpleUUID() + (StrUtil.isBlank(extension) ? StrUtil.EMPTY : StrUtil.DOT + extension);
}
try {
validateStoragePath(properties.getBucketName(), dir, fileName);
} catch (IllegalArgumentException ex) {
log.warn("拒绝包含非法路径的文件上传请求");
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_FILE_PATH_INVALID));
}
Map<String, String> resultMap = new HashMap<>(4);
resultMap.put("bucketName", properties.getBucketName());
resultMap.put("fileName", fileName);
@@ -100,7 +113,7 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> impl
fileLog(file, dir, fileName, groupId, type);
} catch (Exception e) {
log.error("上传失败", e);
return R.failed(e.getLocalizedMessage());
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_FILE_UPLOAD_FAILED));
}
return R.ok(resultMap);
}
@@ -121,6 +134,14 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> impl
fileName = urlFileName;
}
try {
validateStoragePath(properties.getBucketName(), null, fileName);
} catch (IllegalArgumentException ex) {
log.warn("拒绝包含非法路径的文件读取请求");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
SysFile sysFile = baseMapper.selectOne(Wrappers.<SysFile>lambdaQuery().eq(SysFile::getFileName, fileName),
false);
if (Objects.isNull(sysFile)) {
@@ -128,6 +149,14 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> impl
return;
}
try {
validateStoragePath(sysFile.getBucketName(), sysFile.getDir(), sysFile.getFileName());
} catch (IllegalArgumentException ex) {
log.warn("拒绝读取包含非法存储路径的文件记录");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
try (FileObject fileObject = fileTemplate.getObject(sysFile.getBucketName(), sysFile.getDir(), fileName)) {
response.setContentType("application/octet-stream; charset=UTF-8");
// 文件唯一hash
@@ -154,6 +183,14 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> impl
if (Objects.isNull(file)) {
return Boolean.FALSE;
}
try {
validateStoragePath(properties.getBucketName(), null, file.getFileName());
} catch (IllegalArgumentException ex) {
log.warn("拒绝删除包含非法存储路径的文件记录");
return Boolean.FALSE;
}
fileTemplate.removeObject(properties.getBucketName(), file.getFileName());
return this.removeById(id);
}
@@ -258,4 +295,29 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> impl
this.save(sysFile);
}
/**
* 校验文件存储路径,确保文件始终位于配置的存储桶目录内。
*
* @param bucketName 存储桶名称
* @param dir 相对目录
* @param fileName 文件名
*/
private void validateStoragePath(String bucketName, String dir, String fileName) {
if (properties.getType() != FileType.LOCAL) {
return;
}
String basePath = properties.getLocal().getBasePath();
if (StrUtil.isBlank(basePath)) {
throw new IllegalArgumentException(MsgUtils.getMessage(UpmsErrorCodes.SYS_FILE_PATH_INVALID));
}
File storageRoot = new File(basePath);
File bucketRoot = new File(storageRoot, bucketName);
File targetFile = StrUtil.isBlank(dir) ? new File(bucketRoot, fileName)
: new File(new File(bucketRoot, dir), fileName);
FileUtil.checkSlip(storageRoot, bucketRoot);
FileUtil.checkSlip(bucketRoot, targetFile);
}
}