-
Notifications
You must be signed in to change notification settings - Fork 401
Description
Summary
The /file/add API endpoint in the application has a critical unrestricted file upload vulnerability. The full upload call chain lacks effective verification of file suffixes and actual content, and the existing suffix verification logic has fatal flaws. Attackers can upload malicious script files (e.g., JSP) by disguising them as image files, which may lead to remote code execution (RCE) when the files are accessed by the container (e.g., Tomcat), resulting in server compromise and sensitive data leakage.
Vulnerability Location
Core affected interface and complete call chain:
RestFileController.add()- ↓
FileService.upload()- ↓
GlobalFileUploader.upload()- ↓
BaseApiClient.uploadImg(MultipartFile file)- ↓
LocalApiClient.uploadImg(InputStream is, String imageUrl)- ↓
LocalApiClient.createNewFileName()
Vulnerability Details
1. No File Verification in Controller Layer
The /file/add interface only checks if the file array is empty, but has no verification for file type, suffix, size or content:
@RestController
@RequestMapping("/file")
public class RestFileController {
@RequiresPermissions("files")
@PostMapping(value = "/add")
@BussinessLog("添加文件")
public ResponseVO add(MultipartFile[] file) {
if (null == file || file.length == 0) {
return ResultUtil.error("请至少选择一张图片!");
}
int res = fileService.upload(file);
return ResultUtil.success("成功上传" + res + "张图片");
}
}2. No Verification in Service Layer
The FileService.upload() method only checks if the file array is empty and loops to call the uploader, with no file type/suffix verification:
@Override
@Transactional(rollbackFor = Exception.class)
public int upload(MultipartFile[] file) {
if (null == file || file.length == 0) {
throw new GlobalFileException("请至少选择一张图片!");
}
for (MultipartFile multipartFile : file) {
FileUploader uploader = new GlobalFileUploader();
// call LocalApiClient's upload method
uploader.upload(multipartFile, FileUploadType.COMMON.getPath(), true);
}
return file.length;
}3. No Verification in Uploader Layer
The GlobalFileUploader.upload() method only routes to different ApiClients based on configuration, with no file verification:
@Override
public VirtualFile upload(MultipartFile file, String uploadType, boolean save) {
// assuming upload to local, return's type is LocalApiClient. Other case is similar.
ApiClient apiClient = this.getApiClient(uploadType);
VirtualFile virtualFile = apiClient.uploadImg(file); // will upload file
return this.saveFile(virtualFile, save, uploadType);
}
ApiClient getApiClient(String uploadType) {
SysConfigService configService = SpringContextHolder.getBean(SysConfigService.class);
Map<String, Object> config = configService.getConfigs();
String storageType = null;
if (null == config || StringUtils.isEmpty((storageType = (String) config.get(ConfigKeyEnum.STORAGE_TYPE.getKey())))) {
throw new ZhydException("[文件服务]当前系统暂未配置文件服务相关的内容!");
}
ApiClient res = null;
switch (storageType) {
case "local":
String localFileUrl = (String) config.get(ConfigKeyEnum.LOCAL_FILE_URL.getKey()),
localFilePath = (String) config.get(ConfigKeyEnum.LOCAL_FILE_PATH.getKey());
res = new LocalApiClient().init(localFileUrl, localFilePath, uploadType);
break;
case "qiniu":
String accessKey = (String) config.get(ConfigKeyEnum.QINIU_ACCESS_KEY.getKey()),
secretKey = (String) config.get(ConfigKeyEnum.QINIU_SECRET_KEY.getKey()),
qiniuBucketName = (String) config.get(ConfigKeyEnum.QINIU_BUCKET_NAME.getKey()),
baseUrl = (String) config.get(ConfigKeyEnum.QINIU_BASE_PATH.getKey());
res = new QiniuApiClient().init(accessKey, secretKey, qiniuBucketName, baseUrl, uploadType);
break;
...
}
if (null == res) {
throw new GlobalFileException("[文件服务]当前系统暂未配置文件服务相关的内容!");
}
return res;
}4. No Verification in BaseApiClient
The BaseApiClient.uploadImg(MultipartFile file) method only checks if the file is null and gets image info, with no suffix/content verification:
@Override
public VirtualFile uploadImg(MultipartFile file) {
this.check();
if (file == null) {
throw new OssApiException("[" + this.storageType + "]文件上传失败:文件不可为空");
}
try {
VirtualFile res = this.uploadImg(file.getInputStream(), file.getOriginalFilename()); // calling the uploadImg method of the subclass
VirtualFile imageInfo = ImageUtil.getInfo(file);
return res.setSize(imageInfo.getSize())
.setOriginalFileName(file.getOriginalFilename())
.setWidth(imageInfo.getWidth())
.setHeight(imageInfo.getHeight());
} catch (IOException e) {
throw new GlobalFileException("[" + this.storageType + "]文件上传失败:" + e.getMessage());
}
}5. Incomplete and Easily Bypassed Verification in LocalApiClient
5.1 uploadImg Method (No Verification)
The LocalApiClient.uploadImg(InputStream is, String imageUrl) method only handles file storage, with no effective verification:
// imageUrl: In the calling process of the previous step, the passed parameter is file.getOriginalFilename(), which represents the original file name of the uploaded file.
@Override
public VirtualFile uploadImg(InputStream is, String imageUrl) {
this.check();
String key = FileUtil.generateTempFileName(imageUrl); // Generate a temporary file name by appending temp to the real name of the file.
this.createNewFileName(key, this.pathPrefix); // This method assigns the file name with a timestamp added to this.newFileName, but it does not include any verification of the file suffix either.
Date startTime = new Date();
String realFilePath = this.rootPath + this.newFileName; // the final save path
FileUtil.checkFilePath(realFilePath);
try (InputStream uploadIs = StreamUtil.clone(is);
InputStream fileHashIs = StreamUtil.clone(is);
FileOutputStream fos = new FileOutputStream(realFilePath)) {
FileCopyUtils.copy(uploadIs, fos); // Complete upload
}
// Note: The remaining code of this method is omitted but does not affect the vulnerability analysis
}5.3 FileUtil Methods (Weak Suffix Extraction)
The suffix extraction logic relies on the original file name and has no normalization/filtering:
public static String generateTempFileName(String imgUrl) {
return "temp" + getSuffixByUrl(imgUrl);
}
public static String getSuffixByUrl(String imgUrl) {
String defaultSuffix = ".png";
if (StringUtils.isEmpty(imgUrl)) {
return defaultSuffix;
}
String fileName = imgUrl;
if(imgUrl.contains("/")) {
fileName = imgUrl.substring(imgUrl.lastIndexOf("/"));
}
String fileSuffix = getSuffix(fileName);
return StringUtils.isEmpty(fileSuffix) ? defaultSuffix : fileSuffix;
}
public static String getSuffix(String fileName) {
int index = fileName.lastIndexOf(".");
index = -1 == index ? fileName.length() : index;
return fileName.substring(index);
}Key Vulnerability Points
- Incomplete and Late Suffix Verification
- The only suffix check is in createNewFileName() (restricting to jpg/jpeg/png/gif/bmp) but is executed too late in the upload chain
- The verification only checks the last suffix of the original file name, which can be bypassed by spoofed suffixes (e.g., malicious.jsp.jpg)
- No Actual File Content Verification
- The entire chain only verifies file suffixes, not whether the file content matches the declared suffix (e.g., adding JPG binary headers to JSP files can bypass checks)
- Unrestricted File Size
- No file size limits in any layer of the upload chain, allowing DoS attacks via oversized file uploads
- Unsafe Suffix Extraction
- Suffix extraction relies on unfiltered original file names, making it easy for attackers to construct malicious file names
Impact
- Remote Code Execution (RCE): Critical impact - attackers can fully control the server by executing malicious scripts
- Denial of Service (DoS): Oversized file uploads exhaust disk space and disrupt system operation
- Sensitive Data Leakage: Attackers can access database credentials, user data, and business information
- Server Tampering: Malicious code can modify/delete critical system files, causing business paralysis