背景
任意文件读取在 Java Web 项目里并不算新问题,但在实际代码审计中仍然高频出现。尤其是一些后台系统、报表平台、文件预览服务,会把本地文件路径、附件路径、模板路径直接暴露给接口处理。如果路径校验不充分,就可能被读取到配置文件、日志、源码片段或运行环境中的敏感文件。
这类问题的危险点不一定在于单个接口本身,而在于它常常成为后续风险的入口:例如读取数据库配置、密钥文件、日志中的 token、历史备份文件等。本文整理一套偏实战的审计方法,重点放在如何发现、验证和修复,不涉及未授权攻击或对真实目标的利用。
典型场景
常见问题多出现在以下几类功能中:
- 文件下载接口:根据 file、path、name、url 等参数读取本地文件。
- 附件预览接口:PDF、图片、Office 文件在线预览时从磁盘读取文件。
- 日志查看接口:后台提供按文件名查看日志内容的功能。
- 模板加载接口:根据参数选择报表模板、邮件模板、导出模板。
- 临时文件接口:导入导出任务生成临时文件,再由接口返回给用户。
问题代码通常有一个共同特征:用户可控输入最终进入了 File、FileInputStream、Files.readAllBytes、ResourceLoader、ServletContext.getResourceAsStream 等读取逻辑。
问题代码示例
下面是一段简化后的示例代码,业务意图是从指定目录下载附件:
@GetMapping("/download")
public void download(String fileName, HttpServletResponse response) throws IOException {
String baseDir = "/data/app/upload/";
File file = new File(baseDir + fileName);
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName());
try (InputStream in = new FileInputStream(file);
OutputStream out = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
}这类代码看起来只是在固定目录下读文件,但实际风险在于 fileName 完全由用户控制。如果缺少路径规范化和边界校验,攻击者可以构造相对路径跳出上传目录。这里不展开真实环境利用,只从代码层面说明风险来源。
审计思路
审计这类问题时,我一般按“入口参数—路径拼接—文件读取—权限校验—响应输出”这条链路走。
1. 定位入口参数
优先搜索下载、预览、读取相关接口。关键词可以从 Controller 层开始:
download
preview
readFile
getFile
filePath
fileName
path
export
attachment
log如果是 Spring 项目,可以重点看带有这些注解的方法:
@GetMapping
@PostMapping
@RequestMapping
@ResponseBody需要关注参数是否来自 HttpServletRequest、RequestParam、PathVariable、JSON body 或 header。只要外部可控,就应继续追踪。
2. 追踪路径构造逻辑
常见危险写法包括:
new File(baseDir + fileName)
new File(baseDir, fileName)
Paths.get(baseDir + path)
Paths.get(baseDir, path)
resourceLoader.getResource("file:" + path)
servletContext.getResourceAsStream(path)并不是所有拼接都有问题,关键看是否做了可靠的规范化处理以及是否限制在允许目录下。仅判断是否包含“..”通常不够,因为不同系统路径分隔符、URL 编码、多重编码、符号链接都可能造成绕过。
3. 检查权限与业务绑定
很多接口即使路径校验正确,也会存在“越权读取业务文件”的问题。例如用户 A 通过猜测附件 ID 下载用户 B 的附件。审计时要看文件是否与当前登录用户、租户、角色或业务单据绑定。
比较安全的设计是:接口只接收文件 ID,由服务端根据数据库记录查询真实路径,并校验当前用户是否有访问该文件的权限。不要直接让前端传磁盘路径。
4. 确认响应内容是否泄露敏感信息
有些接口会在异常时把绝对路径、堆栈、文件不存在信息直接返回。虽然不一定构成任意文件读取,但会为后续攻击面定位提供信息。建议统一异常返回,不把本地路径暴露给前端。
安全验证方式
在授权测试或本地复现时,不建议直接读取系统敏感文件。可以在测试目录下创建一个哨兵文件,用来判断是否可以跳出预期目录。
/tmp/lab/
├── upload/
│ └── normal.txt
└── sentinel.txt目标接口预期只能读取 upload 目录。如果通过构造参数读取到了 sentinel.txt,就说明存在目录边界问题。这样验证风险足够明确,也避免触碰真实敏感数据。
用于本地搭建测试文件的命令示例:
mkdir -p /tmp/lab/upload
echo "normal file" > /tmp/lab/upload/normal.txt
echo "sentinel file" > /tmp/lab/sentinel.txt修复要点
修复任意文件读取,核心不是简单替换几个字符,而是建立服务端可信边界。
1. 不让用户直接传真实路径
推荐接口只接受文件 ID:
GET /file/download?id=12345服务端从数据库中查询文件记录,拿到存储路径、所属用户、业务类型,再做权限校验。这样能减少路径被用户直接控制的机会。
2. 使用规范化路径并校验目录边界
如果确实需要接收文件名,应使用 Path.normalize 和 toRealPath 做路径规范化,再判断最终路径是否仍在允许目录下。
public Path resolveSafePath(String baseDir, String userInput) throws IOException {
Path basePath = Paths.get(baseDir).toRealPath().normalize();
Path targetPath = basePath.resolve(userInput).normalize();
// 文件存在时可使用 toRealPath 处理符号链接
if (Files.exists(targetPath)) {
targetPath = targetPath.toRealPath().normalize();
}
if (!targetPath.startsWith(basePath)) {
throw new SecurityException("invalid file path");
}
return targetPath;
}注意:只使用 normalize 不一定能处理符号链接问题。若允许目录中存在软链接,可能指向目录外部。对高风险场景,建议禁用上传目录中的符号链接,或在读取前使用 toRealPath 校验真实路径。
3. 限制文件名格式
如果业务只允许下载上传后的普通附件,可以限制文件名为服务端生成的 UUID 或哈希名,而不是原始文件名。
^[a-f0-9]{32}\.(jpg|png|pdf|docx)$白名单比黑名单可靠。不要只过滤 ../,也不要只替换路径分隔符后就认为安全。
4. 做好权限校验
下载文件前至少确认这些信息:
- 当前用户是否已登录。
- 文件是否属于当前用户或当前租户。
- 当前角色是否有下载该业务文件的权限。
- 文件状态是否允许下载,例如是否已删除、是否已归档。
权限校验应放在服务端,不能依赖前端隐藏按钮。
5. 控制返回头与 MIME 类型
文件下载接口还应避免响应头注入、内容嗅探等问题。文件名进入 Content-Disposition 前应进行安全处理。
String safeName = URLEncoder.encode(displayName, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + safeName);
response.setHeader("X-Content-Type-Options", "nosniff");常见误区
- 误区一:只判断参数中是否包含“..”。这种方式容易遗漏编码、分隔符差异和符号链接场景。
- 误区二:认为后台接口就不需要校验。后台账号一旦被低权限用户使用,文件读取风险仍然存在。
- 误区三:认为文件名由数据库取出就一定安全。数据库中的路径如果最初来自用户输入,仍然要校验。
- 误区四:只修 Controller,不修公共文件服务。很多项目存在 FileService、StorageService 这类公共封装,应该在底层统一兜底。
代码审计检查清单
| 检查项 | 关注点 |
| 外部输入 | file、path、name、url、id 是否可控 |
| 路径拼接 | 是否直接拼接本地路径 |
| 规范化 | 是否 normalize、toRealPath,并校验 startsWith |
| 符号链接 | 是否可能通过软链接跳出目录 |
| 权限校验 | 文件是否与用户、租户、业务对象绑定 |
| 异常处理 | 是否泄露绝对路径、堆栈、内部目录结构 |
| 响应头 | Content-Disposition 是否处理特殊字符 |
日志与监控建议
生产环境中,建议对文件读取接口记录必要审计日志,但不要记录完整敏感路径或文件内容。可记录用户 ID、租户 ID、文件 ID、业务类型、读取结果、客户端 IP、时间等信息。
如果出现大量异常路径、频繁访问不存在文件、短时间内批量下载不同业务附件,应触发告警。这类行为不一定说明已经被入侵,但值得排查。
总结
任意文件读取漏洞的根因通常不是某一个 API 不安全,而是把用户输入直接带入了本地文件系统。比较稳妥的修复路线是:接口只接收业务 ID,服务端查询真实路径;读取前做权限校验;路径解析后确认仍在允许目录内;统一处理异常和响应头。
实际审计时不要只看单个下载接口,建议把项目中的文件服务封装、附件表设计、权限模型一起看。很多问题表面是路径穿越,实质是文件访问控制缺失。
推荐意见