背景
任意文件读取是 Java Web 项目里比较常见、但经常被低估的一类问题。它不一定直接导致代码执行,但如果能读取配置文件、密钥文件、日志或源码,后续风险会明显放大,例如数据库凭据泄露、JWT 密钥泄露、云服务 AK/SK 泄露等。
这篇文章整理一次代码审计中遇到的文件下载接口问题。为了避免误导,文中示例均为脱敏后的本地测试代码,重点放在漏洞成因、复现思路、修复方式和排查清单上,不涉及真实目标。
问题场景
某业务系统提供附件下载功能,接口大致形式如下:
GET /api/file/download?name=report.pdf服务端根据 name 参数拼接文件路径,然后把文件内容写回响应。开发初衷是只允许下载某个固定目录下的业务附件,但代码中没有对路径穿越做严格限制。
脱敏后的问题代码类似这样:
String baseDir = "/data/app/upload/";
String name = request.getParameter("name");
File file = new File(baseDir + name);
if (!file.exists()) {
response.setStatus(404);
return;
}
try (InputStream in = new FileInputStream(file);
OutputStream out = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}这类代码在业务上看起来很直观,但问题在于:用户输入直接参与了文件路径拼接。如果攻击者传入包含 ../ 的路径片段,就可能跳出预期目录。
技术分析
漏洞核心不是“下载接口”本身,而是服务端没有验证最终访问的真实路径是否仍位于允许目录内。常见风险点包括:
- 直接使用字符串拼接路径,例如
baseDir + name。 - 只判断文件是否存在,没有判断文件是否在白名单目录中。
- 简单过滤
../,但没有考虑编码、反斜杠、重复分隔符等情况。 - 允许用户传入完整路径,如
/var/log/app.log或 Windows 下的盘符路径。 - 下载接口没有鉴权,或者只鉴权用户登录状态,不校验文件归属关系。
在 Java 中,File 和 Path 都可能受到相对路径影响。仅使用 getPath() 或拼接字符串无法确认最终落点,应该使用规范化后的绝对路径进行判断。
例如:
Path basePath = Paths.get("/data/app/upload").toRealPath();
Path targetPath = basePath.resolve(userInput).normalize();这里 normalize() 会处理路径中的 . 和 .. 片段,但仅 normalize 还不够,后续必须检查目标路径是否以基准目录开头。
本地复现思路
在本地环境可以构造一个最小示例验证问题。目录结构如下:
/tmp/demo/
├── upload/
│ └── report.pdf
└── secret.txt如果下载接口的基准目录是 /tmp/demo/upload/,正常情况下只能读取 report.pdf。但在存在缺陷的实现中,传入相对路径可能访问到上级目录文件。
GET /api/file/download?name=../secret.txt如果响应返回了 secret.txt 内容,就说明接口没有限制最终路径边界。实际测试时建议只在授权环境、本地靶场或测试系统中进行,不要对未知系统做探测。
安全修复方案
比较稳妥的修复方式是:固定下载根目录,对用户输入做基本校验,解析真实路径后确认其仍在根目录下,同时增加文件归属和权限校验。
示例代码如下:
Path basePath = Paths.get("/data/app/upload").toRealPath();
String name = request.getParameter("name");
if (name == null || name.isBlank()) {
response.setStatus(400);
return;
}
// 可选:限制只允许业务文件名,不允许路径分隔符
if (name.contains("/") || name.contains("\\")) {
response.setStatus(400);
return;
}
Path targetPath = basePath.resolve(name).normalize();
// 关键检查:规范化后必须仍在 basePath 下
if (!targetPath.startsWith(basePath)) {
response.setStatus(403);
return;
}
// 如果需要防止符号链接绕过,可进一步获取真实路径
if (!Files.exists(targetPath) || !Files.isRegularFile(targetPath)) {
response.setStatus(404);
return;
}
Path realTargetPath = targetPath.toRealPath();
if (!realTargetPath.startsWith(basePath)) {
response.setStatus(403);
return;
}
try (InputStream in = Files.newInputStream(realTargetPath);
OutputStream out = response.getOutputStream()) {
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"download.bin\"");
in.transferTo(out);
}如果业务允许按目录下载文件,不建议只靠字符串黑名单过滤。可以维护一张附件表,下载时传入附件 ID,再由服务端根据数据库中的存储路径读取文件。这样用户不能直接控制路径,风险会小很多。
更推荐的业务设计
生产环境中,附件下载接口最好不要直接暴露真实文件名或相对路径。推荐设计为:
- 上传时生成随机存储名,例如 UUID,原始文件名只用于展示。
- 数据库记录附件 ID、存储路径、创建人、业务对象 ID、文件大小、哈希值。
- 下载时只接收附件 ID,不接收文件路径。
- 服务端校验当前用户是否有权限访问该附件所属业务对象。
- 文件读取前校验真实路径必须位于存储根目录内。
接口可以设计成:
GET /api/file/download?id=8f3c2c1a9e4b4e0f服务端流程大致如下:
1. 根据 id 查询附件记录
2. 判断附件是否存在、是否已删除
3. 校验当前用户对业务对象是否有访问权限
4. 使用服务端保存的相对路径解析文件
5. 校验真实路径边界
6. 输出文件内容代码审计检查点
审计 Java Web 项目时,可以重点搜索以下关键字和调用点:
new File(
FileInputStream
Files.newInputStream
Files.readAllBytes
ResourceUtils.getFile
getResourceAsStream
response.getOutputStream
Content-Disposition
MultipartFile.transferTo
Paths.get发现文件读取或下载逻辑后,可以按以下问题逐项确认:
- 用户是否能控制文件名、相对路径或完整路径?
- 是否存在固定基准目录?
- 是否使用
normalize()、toRealPath()处理路径? - 是否校验目标路径必须在基准目录下?
- 是否考虑符号链接问题?
- 是否校验文件归属关系和用户权限?
- 是否限制可下载文件类型和文件大小?
- 异常信息是否会泄露服务器绝对路径?
常见错误修复方式
实际项目中经常看到一些“看起来修了,实际没修干净”的处理方式:
- 只替换
../:容易被编码、反斜杠、重复分隔符等方式绕过。 - 只判断
name.contains(".."):可能误伤正常文件名,也没有解决绝对路径问题。 - 只做前端限制:请求可以直接构造,前端校验没有安全边界意义。
- 只判断后缀名:后缀名不能证明路径安全。
- 把异常堆栈返回给前端:会暴露服务器目录结构,增加后续风险。
更可靠的做法仍然是:服务端控制路径来源,真实路径规范化,检查目录边界,再叠加权限校验。
日志与监控建议
这类问题修复后,也建议补充一些日志和监控规则。比如出现以下特征时,可以记录安全日志并触发告警:
- 下载参数中包含
../、..\、URL 编码后的路径穿越片段。 - 短时间内同一账号或 IP 请求大量不存在文件。
- 请求目标包含常见敏感文件名,如配置文件、日志文件、密钥文件名等。
- 下载失败状态码异常升高。
日志中不建议记录完整敏感路径或文件内容,只记录必要字段,例如用户 ID、业务 ID、参数摘要、请求来源、处理结果等。
总结
任意文件读取问题的成因通常并不复杂,但影响面取决于系统内保存了什么敏感信息。很多时候,一个不起眼的附件下载接口,就可能成为配置泄露、凭据泄露的入口。
从代码审计角度看,判断这类问题主要抓三点:用户是否控制路径、最终路径是否限制在基准目录、是否有业务权限校验。修复时不要依赖简单黑名单过滤,尽量改为附件 ID 映射、服务端保存路径、真实路径边界校验的模式。这个思路比临时补几个字符串过滤更稳,也更适合长期维护。
推荐意见