跳转到帖子

背景

任意文件读取在 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、后端查映射、真实路径校验、最小权限运行。这样即使某一层出现疏漏,也不至于让问题快速扩大。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…