介绍
本篇为代码审计系列的第八篇《代码审计之文件下载》,预计本系列为30篇左右。
对于一些场景下,系统功能可能需要查看一些文件的内容,或者去下载一些文件,比如说查看日志功能,程序会读取对应的日志文件过来,再比如下载附件功能或者下载一些办公文档的功能,类似于这种获取文件内容的或者直接下载文件的,当未对相关路径做严格过滤时,就会导致任意文件下载问题。
示例
我们来看下存在问题的文件下载代码是怎么样的,示例如下:
@RestController
publicclassDownloadController{
// 存在任意文件下载漏洞的接口
@GetMapping("/download")
public ResponseEntity<byte[]> downloadFile(@RequestParam("file") String filePath) throws IOException {
File file = new File(filePath); // 漏洞点:直接使用未验证的用户输入作为文件路径
// 读取文件内容
FileInputStream fis = new FileInputStream(file);
byte[] bytes = newbyte[(int) file.length()];
fis.read(bytes);
fis.close();
// 设置响应头强制下载
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename=" + file.getName());
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes);
}
}
上面代码直接接收了前端传来的filePath参数值,然后去获取了该值所指的文件内容进行了返回,因为没有存在过滤和校验,会导致任意文件下载问题。
修复方式上,我们可以把下载的路径去固定一下,判断最终的路径是不是以我们指定的路径开头的,如下修复后的代码:
privatestaticfinal String BASE_DIR = "/var/log/";
@GetMapping("/secure-download")
public ResponseEntity<byte[]> secureDownload(
@RequestParam("file") String filename) throws Exception {
// 1. 路径规范化处理(防御路径遍历)
Path resolvedPath = Paths.get(BASE_DIR, filename).normalize();
// 2. 验证路径是否在允许的目录范围内
if (!resolvedPath.startsWith(BASE_DIR)) {
thrownew SecurityException("非法文件路径访问");
}
File file = new File(resolvedPath);
// 读取文件内容
FileInputStream fis = new FileInputStream(file);
byte[] bytes = newbyte[(int) file.length()];
fis.read(bytes);
fis.close();
// 设置响应头强制下载
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename=" + file.getName());
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes);
}
}
其中Paths.get接收的是我们固定好的路径和传入的路径,经过normalize规范化后,会解析路径中的../这种,比如我固定路径是/var/logs/然后接收的路径是../../../../../etc/passwd,那么最后Paths返回的路径就是/var/logs/../../../../../etc/passwd,../解析后最后的路径就是/etc/passwd。
到这一步是不能阻止任意文件下载的,重要的在下面那个if判断,我们会判断Paths返回的路径是否以/var/logs开头,从而限定了下载目录。
这种方式是优先推荐的,如果目录不能固定,还有一种措施是可以限制文件名或文件后缀,以白名单形式来进行校验。
// 白名单配置
privatestaticfinal Set<String> ALLOWED_EXTENSIONS = Set.of("pdf", "txt", "jpg");
privatestaticfinal Set<String> ALLOWED_FILENAMES = Set.of("license.txt", "README.md");
@GetMapping("/secure-download")
public ResponseEntity<byte[]> secureDownload(
@RequestParam("file") String filename) throws Exception {
Path path= Paths.get(filename);
if (!isFilenameAllowed(path.getFileName().toString())) {
thrownew SecurityException("文件名不在允许列表中");
}
if (!isExtensionAllowed(path.getFileName().toString())) {
thrownew SecurityException("文件类型被禁止");
}
File file = new File(path);
// 读取文件内容
FileInputStream fis = new FileInputStream(file);
byte[] bytes = newbyte[(int) file.length()];
fis.read(bytes);
fis.close();
// 设置响应头强制下载
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename=" + file.getName());
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes);
}
}
其中判断文件名的函数如下:
privatebooleanisFilenameAllowed(String filename){
return ALLOWED_FILENAMES.contains(filename);
}
判断文件后缀的代码如下:
privatebooleanisExtensionAllowed(String filename){
String extension = extractExtension(filename).toLowerCase();
return ALLOWED_EXTENSIONS.contains(extension);
}
private String extractExtension(String filename){
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
return""; // 无扩展名或以点结尾
}
return filename.substring(lastDotIndex + 1);
}
另外,最好不要用黑名单过滤,比如检测是否存在../这种,这种可能存在一些绕过方式,比如路径编码或者传入绝对路径等。
以上就是关于代码审计中任意文件上传的相关内容,感谢阅读。
关于我们
我们是《AI安全攻防》,致力于分享AI安全、渗透测试、代码审计等内容,欢迎您的关注!
原文始发于微信公众号(AI安全攻防):代码审计之文件下载
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论