介绍
本篇为代码审计系列的第七篇《代码审计之文件上传》,预计本系列为30篇左右。
对于文件上传功能,如果后端没有对上传的文件做过滤和校验,则会导致可上传任意文件到服务器,如果后端根据文件名去拼接一个路径,把文件传到对应目录下,当文件名没有进行过滤时,也可造成文件传到任意目录下,相当于上一节说的目录遍历。
示例代码
文件上传目前常用的方式就是MultipartFile,示例代码如下:
privatestaticfinal String UPLOADED_FOLDER = System.getProperty("user.dir") + "/src/main/resources/static/file/";
@GetMapping("/uploadStatus")
public String uploadStatus(){
return"upload";
}
@ApiOperation(value = "vul:上传任意文件")
@PostMapping("/uploadVul")
public String uploadVul(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("message", "请选择要上传的文件");
return"redirect:uploadStatus";
}
try {
byte[] bytes = file.getBytes();
Path dir = Paths.get(UPLOADED_FOLDER);
// path定义了文件所保存的目录,未对文件名做过滤
Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
// 未对文件做校验,直接进行了上传
Files.write(path, bytes);
redirectAttributes.addFlashAttribute("message",
"上传成功:" + path);
} catch (Exception e) {
return e.toString();
}
return"redirect:uploadStatus";
}
上述代码在接收到前端传来的文件时,直接进行了写入的操作,未对文件做类型判断,所以存在文件上传问题,同时,文件所存的目录直接拼接了文件名,未对文件名做过滤,可导致文件传到任意目录下面。
下面我们来看一个比较全面防护的示例代码,对文件后缀、大小、文件名都进行了相关过滤:
@RestController
@RequestMapping("/file")
@Api(tags = "FileController", description = "文件上传接口")
publicclassFileController{
privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(FileController.class);
// 允许上传的文件后缀(白名单)
privatestaticfinal Set<String> ALLOWED_EXTENSIONS = new HashSet<>(Arrays.asList(
"jpg", "jpeg", "png", "gif"
));
// 文件大小限制(10MB)
privatestaticfinallong MAX_FILE_SIZE = 10 * 1024 * 1024;
@Value("${file.upload.path:/upload}")
private String uploadPath;
@PostMapping("/upload")
@ApiOperation("文件上传")
public CommonResult<String> uploadFile(@RequestParam("file") MultipartFile file) {
try {
// 1. 空文件检查
if (file.isEmpty()) {
return CommonResult.failed("上传失败:文件为空");
}
// 2. 文件大小检查
if (file.getSize() > MAX_FILE_SIZE) {
return CommonResult.failed("上传失败:文件大小超过限制");
}
// 3. 获取文件名和后缀
String originalFilename = file.getOriginalFilename();
String extension = getFileExtension(originalFilename);
// 4. 文件后缀检查
if (!isValidFileExtension(extension)) {
return CommonResult.failed("上传失败:不支持的文件类型");
}
// 5. 生成新的文件名(防止文件名冲突)
String newFilename = generateUniqueFilename(extension);
// 6. 确保上传目录存在
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
// 7. 构建完整的文件路径
Path filePath = Paths.get(uploadPath, newFilename);
// 8. 保存文件
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
return CommonResult.success(newFilename, "上传成功");
} catch (IOException e) {
LOGGER.error("文件上传失败", e);
return CommonResult.failed("上传失败:" + e.getMessage());
}
}
/**
* 获取文件后缀(小写)
*/
private String getFileExtension(String filename){
if (filename == null || filename.lastIndexOf(".") == -1) {
return"";
}
return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
}
/**
* 验证文件后缀是否合法
*/
privatebooleanisValidFileExtension(String extension){
return ALLOWED_EXTENSIONS.contains(extension.toLowerCase());
}
/**
* 生成唯一的文件名
*/
private String generateUniqueFilename(String extension){
String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String random = UUID.randomUUID().toString().substring(0, 8);
return timestamp + "_" + random + "." + extension;
}
上面这个代码通过白名单验证了文件后缀,这种是最安全的一种方式,然后文件在保存时,为了避免文件名重复,会重命名,这样就不会存在目录遍历的问题。
如果不用随机命名,则需要对文件名进行校验,这种情况的防护代码和目录遍历中的就基本一样了,可以参考目录遍历章节,另外,最好用后缀的检测方式,如果只检测Conent-Type,则依然可以传任意文件,比如下面这段代码只检测Content-Type:
String contentType = file.getContentType();
if (!"image/jpeg".equals(contentType) && !"image/png".equals(contentType)) {
redirectAttributes.addFlashAttribute("message", "不允许上传该类型文件!");
return"redirect:uploadStatus";
}
以上就是关于代码审计中任意文件上传的相关内容,感谢阅读。
关于我们
我们是《AI安全攻防》,致力于分享AI安全、渗透测试、代码审计等内容,欢迎您的关注!
原文始发于微信公众号(AI安全攻防):代码审计之文件上传
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论