【Java代码审计 | 第八篇】文件操作漏洞成因及防范
未经许可,不得转载。
文章目录
- 文件操作漏洞
- 文件读取漏洞
- 基于 InputStream 的读取
- 基于 FileReader 的读取
- 文件下载漏洞
- 文件删除漏洞
- 防范
文件操作漏洞
分为文件读取漏洞、文件下载漏洞与文件删除漏洞。
文件读取漏洞
在Java中,文件读取通常有两种常见方式:一种是基于InputStream
,另一种是基于FileReader
。
漏洞成因:未对用户输入做过滤,导致读取敏感文件并返回至客户端。
以下两种代码形式都存在路径遍历问题。
基于 InputStream 的读取
String filename = request.getParameter("filename"); // 假设这是用户输入的文件名
File file = new File(filename); // 创建文件对象,未进行任何路径验证
InputStream inputStream = new FileInputStream(file); // 创建输入流
int len;
while (-1 != (len = inputStream.read())) { // 循环读取文件内容
outputStream.write(len); // 将读取的字节写入输出流
}
基于 FileReader 的读取
String filename = request.getParameter("filename"); // 假设这是用户输入的文件名
String fileContent = ""; // 存储文件内容
FileReader fileReader = new FileReader(filename); // 创建FileReader对象,未进行任何路径验证
BufferedReader bufferedReader = new BufferedReader(fileReader); // 包装为BufferedReader
String line;
while (null != (line = bufferedReader.readLine())) { // 逐行读取文件
fileContent += (line + "\n"); // 拼接每一行内容
}
文件下载漏洞
漏洞成因:未对用户输入做过滤,导致用户端可下载敏感文件。
filename 参数未经过任何验证或过滤,攻击者可以通过构造恶意路径下载系统敏感文件。
String filename = request.getParameter("filename"); // 获取文件名
File file = new File(filename); // 创建文件对象
response.reset(); // 重置响应
response.addHeader("Content-Disposition", "attachment;filename=" + new String(filename.getBytes("utf-8"))); // 设置下载文件名
response.addHeader("Content-Length", "" + file.length()); // 设置文件大小
response.setContentType("application/octet-stream; charset=utf-8"); // 设置响应内容类型为二进制流
InputStream inputStream = new FileInputStream(file); // 创建输入流读取文件
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream()); // 获取输出流
int len;
while (-1 != (len = inputStream.read())) { // 读取文件并写入响应
outputStream.write(len);
}
inputStream.close(); // 关闭输入流
outputStream.close(); // 关闭输出流
文件删除漏洞
漏洞成因:未对用户输入做过滤,导致敏感文件被删除。
filename 参数未经过任何验证或过滤,攻击者可以通过构造恶意路径删除系统关键文件。
String filename = request.getParameter("filename"); // 获取文件名
File file = new File(filename); // 创建文件对象
if (file != null && file.exists() && file.delete()) { // 检查文件存在并删除
response.getWriter().println("Delete success"); // 删除成功
} else {
response.getWriter().println("Delete failed"); // 删除失败
}
防范
1、使用 getCanonicalPath() 方法获取文件的规范路径,并与预期的基路径进行比较,确保文件路径在允许的范围内。
String basePath = "/allowed/directory/"; // 允许访问的基路径
File file = new File(basePath, filename); // 拼接文件路径
if (!file.getCanonicalPath().startsWith(new File(basePath).getCanonicalPath())) {
throw new SecurityException("非法文件路径"); // 路径不在允许范围内,抛出异常
}
对以下语句进行解读:
if (!file.getCanonicalPath().startsWith(new File(basePath).getCanonicalPath())) {
file.getCanonicalPath():这个方法返回文件的规范化路径,即将路径中的 .(当前目录)和 …(上级目录)等符号解析成实际的绝对路径。
1、假设:basePath = “/allowed/directory/”
2、用户输入 filename = “…/…/etc/passwd”
3、new File(basePath):创建 /allowed/directory/ 的 File 对象。
4、file = new File(basePath, filename):用户提供的路径是 …/…/etc/passwd,所以拼接后的 file 实际路径为 /allowed/directory/…/…/etc/passwd,这是一个潜在的路径遍历。
5、file.getCanonicalPath():获取文件的规范化路径,经过解析后变为 /etc/passwd(因为路径 …/…/ 会使路径跳转到根目录)。
6、new File(basePath).getCanonicalPath():获取 /allowed/directory/ 的规范化路径,即 /allowed/directory/。
7、在这种情况下,file.getCanonicalPath()(即 /etc/passwd)不会以 /allowed/directory/ 开头,因此会进入 if 语句块,阻止访问该文件。
其次是检查文件后缀是否在允许的后缀白名单中,举个例子:
// 定义允许的文件后缀白名单
Set<String> allowedExtensions = new HashSet<>(Arrays.asList(
"txt", "pdf", "jpg", "png", "doc", "xls"
));
String filename = request.getParameter("filename"); // 获取文件名
String fileExtension = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase(); // 提取后缀并转为小写
// 检查后缀是否在白名单中
if (!allowedExtensions.contains(fileExtension)) {
throw new SecurityException("非法文件类型"); // 文件类型不在白名单中,抛出异常
}
File file = new File(filename); // 创建文件对象
// 使用 try-with-resources 读取文件
try (InputStream inputStream = new FileInputStream(file);
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream())) {
int len;
while (-1 != (len = inputStream.read())) {
outputStream.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}