背景
任意文件读取在 Java Web 项目里并不少见,尤其是历史系统中存在大量下载、预览、导出接口时,很容易因为路径拼接不当引入问题。它通常不像 RCE 那样显眼,但实际影响不低:配置文件、日志、源码片段、密钥文件一旦被读取,后续风险会被放大。
这篇文章整理一次偏通用的代码审计过程,重点放在如何定位问题、如何确认风险边界,以及如何修复。文中的代码和路径均做了脱敏处理,不涉及真实目标。
场景描述
某 Java Web 系统提供文件预览接口,前端传入文件名,后端从指定目录读取并返回内容。功能本身很常见,例如读取用户上传的附件、导出报表或查看模板文件。
问题出在后端只做了简单字符串拼接,没有限制最终读取路径必须位于业务目录内。典型代码如下:
String basePath = "/data/app/files/";
String fileName = request.getParameter("file");
File file = new File(basePath + fileName);
if (!file.exists()) {
response.setStatus(404);
return;
}
try (InputStream in = new FileInputStream(file);
OutputStream out = response.getOutputStream()) {
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
}这类代码在审计时非常值得关注:参数名可能是 file、path、name、url、template、report、attachId 等,接口名可能包含 download、preview、read、export、view。
问题分析
核心问题是后端把用户输入直接参与文件路径计算。攻击者不需要控制服务器,只要构造包含目录跳转符的输入,就可能让程序读取业务目录之外的文件。
从代码层面看,风险点主要有三个:
- 直接拼接路径:使用 basePath + fileName 形成文件路径。
- 缺少规范化校验:没有使用 canonical path 判断最终路径是否仍在允许目录内。
- 访问控制不足:接口只校验文件是否存在,没有校验当前用户是否有权限读取该文件。
很多系统会加一层看似有效的过滤,例如替换 ../,但这类黑名单处理容易遗漏编码、分隔符差异和多次解析场景,不建议作为唯一防护。
审计定位方法
在代码审计中,可以从以下几个方向搜索。重点不是机械匹配某个危险函数,而是看“外部输入是否影响了文件系统访问目标”。
// 常见关键词
new File(
FileInputStream
Files.readAllBytes
Files.copy
Paths.get
getResourceAsStream
ResourceUtils.getFile
MultipartFile.transferTo
response.getOutputStream如果项目较大,可以结合调用链看参数来源,例如 controller 层的 @RequestParam、@PathVariable、request.getParameter,以及 JSON body 反序列化后的字段。
@GetMapping("/preview")
public void preview(@RequestParam String file, HttpServletResponse response) {
fileService.read(file, response);
}继续跟进到 service 层,确认 file 参数是否进入文件读取、模板解析、压缩解压、图片处理等逻辑。如果中间只做了扩展名判断或字符串替换,仍然要谨慎。
安全复现思路
漏洞复现建议在本地测试环境完成,不要对第三方系统进行未授权测试。为了验证问题,可以构造一个最小化的本地目录:
mkdir -p /tmp/demo/files
printf 'normal file' > /tmp/demo/files/a.txt
printf 'secret config' > /tmp/demo/secret.txt示例接口如果将 basePath 设置为 /tmp/demo/files/,正常读取 a.txt 没问题。但如果传入相对路径跳转,程序可能访问到 /tmp/demo/secret.txt。这说明文件访问已经逃逸出预期目录。
复现时建议记录三类信息:
- 可读取范围:只能读取应用目录,还是能读取系统其他路径。
- 权限边界:读取行为受运行用户权限限制,通常取决于 Web 服务进程账号。
- 敏感文件类型:配置、日志、临时文件、备份文件、密钥材料等。
注意:复现的目的应是确认漏洞存在和影响范围,避免读取不必要的敏感内容。安全测试中保留最小证据即可。
常见错误修复方式
下面这些修复方式在真实项目中经常见到,但并不可靠。
- 只判断是否包含 ../:容易被不同编码、不同分隔符或多次拼接逻辑绕过。
- 只限制扩展名:例如只允许 .txt,但仍可能读取业务目录外的同类型文件。
- 只使用 getAbsolutePath:绝对路径不等于安全路径,仍可能包含未解析的目录跳转。
- 只依赖前端限制:后端接口必须独立完成校验。
路径安全的重点是“最终解析后的真实路径是否在允许目录下”,而不是“原始输入看起来是否正常”。
推荐修复方案
比较稳妥的做法是使用 Path API 做路径解析和归一化,并校验最终路径必须位于基准目录内。
Path baseDir = Paths.get("/data/app/files").toRealPath(LinkOption.NOFOLLOW_LINKS);
String fileName = request.getParameter("file");
Path target = baseDir.resolve(fileName).normalize();
Path realTarget = target.toRealPath(LinkOption.NOFOLLOW_LINKS);
if (!realTarget.startsWith(baseDir)) {
response.setStatus(403);
return;
}
if (!Files.isRegularFile(realTarget)) {
response.setStatus(404);
return;
}
try (InputStream in = Files.newInputStream(realTarget);
OutputStream out = response.getOutputStream()) {
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
}这里有几个细节:
- baseDir 使用 toRealPath 获取真实路径,避免基准目录本身是软链接导致判断失真。
- resolve 后再 normalize,用于处理普通的相对路径片段。
- realTarget 使用 toRealPath,确认最终文件真实位置。
- startsWith 判断必须基于 Path 对象,不建议用字符串 startsWith。
- Files.isRegularFile 用于排除目录、特殊文件等情况。
如果业务允许,最好不要让用户直接传文件路径,而是传文件 ID。后端通过数据库映射到实际存储路径,并校验当前用户是否拥有该文件权限。
// 更推荐的设计:用户只传 fileId
@GetMapping("/download")
public void download(@RequestParam Long fileId, Principal principal) {
FileRecord record = fileRepository.findById(fileId);
if (record == null || !record.getOwnerId().equals(principal.getName())) {
throw new AccessDeniedException("no permission");
}
// 后续再读取 record 中受控的存储路径
}权限与日志建议
除了修代码,还需要补齐运行时控制。
- 服务进程使用低权限账号运行,避免拥有读取系统敏感目录的权限。
- 上传目录、导出目录与应用配置目录分离,降低横向读取影响。
- 下载和预览接口记录文件 ID、用户 ID、客户端 IP、结果状态,不建议完整记录原始敏感路径。
- 对连续异常路径请求做告警,例如包含目录跳转符、绝对路径、异常分隔符等。
- 定期清理临时文件、备份文件和导出文件,减少可被读取的数据面。
检测清单
| 检查项 | 说明 |
|---|---|
| 外部输入是否进入文件 API | 关注下载、预览、导出、模板、压缩解压等功能 |
| 是否使用真实路径校验 | 应使用 canonical path 或 Path.toRealPath 后判断边界 |
| 是否存在软链接影响 | 上传目录允许创建软链接时要特别注意 |
| 是否有用户级权限校验 | 不能只判断文件存在,还要判断用户是否可访问 |
| 是否记录访问日志 | 便于后续溯源和发现异常扫描 |
总结
任意文件读取的根因通常不复杂,但实际影响容易被低估。审计时可以围绕“外部输入是否控制文件路径”这条主线展开,再结合路径规范化、软链接、权限校验判断风险。
修复上不建议依赖黑名单过滤,更可靠的方式是:用户传 ID、后端查映射、真实路径校验、最小权限运行。这样即使某一层出现疏漏,也不至于让问题快速扩大。
推荐意见