跳转到帖子

背景

做 Java Web 代码审计时,很多问题并不是一眼看到危险函数就能确认的。真实项目里通常有多层 Controller、Service、DAO、Filter、Interceptor,还有各种参数绑定和工具类封装。单点搜索容易漏报,也容易误报。

这篇文章整理一个偏通用的审计思路:围绕“入口参数如何流向敏感操作”做数据流追踪。内容以本地靶场或授权测试环境为前提,重点放在漏洞确认、修复建议和审计方法,不涉及未授权利用和攻击扩展。

问题场景

假设某 Java Web 项目提供了一个文件预览接口,业务上允许用户查看自己上传目录下的文档。代码经过多层封装后,大概流程如下:

  • Controller 接收参数 fileName;
  • Service 拼接上传目录路径;
  • 工具类读取文件并返回内容;
  • 前端根据响应展示预览结果。

这类功能的常见风险包括路径穿越、任意文件读取、越权访问以及日志泄露。审计时不能只看是否调用了 FileInputStream,还要确认用户输入是否经过规范化、边界校验和权限校验。

核心代码示例

下面是一段简化后的示例代码,便于说明问题。实际项目中可能散落在不同类里。

@GetMapping("/preview")
public ResponseEntity<String> preview(@RequestParam String fileName,
                                      HttpServletRequest request) throws IOException {
    Long userId = getCurrentUserId(request);
    String content = filePreviewService.readUserFile(userId, fileName);
    return ResponseEntity.ok(content);
}

public String readUserFile(Long userId, String fileName) throws IOException {
    String baseDir = uploadRoot + "/" + userId + "/";
    File file = new File(baseDir + fileName);
    return FileUtils.readFileToString(file, StandardCharsets.UTF_8);
}

这段代码的问题在于,fileName 直接参与路径拼接。即使 baseDir 按用户 ID 隔离,攻击者仍可能通过特殊路径片段尝试跳出目录边界。审计时应关注“最终访问的真实路径是否仍在允许目录内”。

技术分析:从入口到敏感点

我一般按以下顺序追踪:

  • 入口点:Controller 参数、JSON Body、Header、Cookie、上传文件名、批量导入内容。
  • 传播路径:DTO 绑定、BeanUtils 拷贝、Map 传参、Service 层拼接、工具类封装。
  • 安全处理:白名单校验、路径规范化、权限判断、编码处理、异常处理。
  • 敏感点:文件读写、命令执行、SQL 拼接、模板渲染、反序列化、反射调用、URL 请求。

以文件读取为例,单纯判断是否包含 “..” 并不稳妥。不同操作系统路径分隔符、URL 编码、重复编码、符号链接、相对路径都会影响结果。更可靠的方式是基于 canonical path 或 normalized path 做目录边界判断。

关键复现步骤

在授权环境中,可以用最小化方式验证问题是否存在。建议不要直接读取敏感系统文件,而是先在测试目录中放置标记文件,验证是否能越过业务目录边界。

# 测试目录示例
/tmp/app/uploads/1001/a.txt
/tmp/app/marker.txt

如果业务预期只能读取 /tmp/app/uploads/1001/ 下的文件,而通过构造参数能够读取到 /tmp/app/marker.txt,就说明目录边界控制存在缺陷。测试时重点记录:

  • 当前用户 ID 对应的业务目录;
  • 目标文件是否属于该用户目录;
  • 服务端最终解析到的真实路径;
  • 是否经过权限判断和审计日志记录。

为了避免误判,可以在服务端临时增加调试日志,仅输出 canonical path,不输出文件内容:

File base = new File(uploadRoot, String.valueOf(userId));
File target = new File(base, fileName);

log.info("basePath={}", base.getCanonicalPath());
log.info("targetPath={}", target.getCanonicalPath());

注意:调试日志不要在生产环境长期保留完整路径信息,路径、用户名、业务 ID 等都可能成为后续攻击的辅助信息。

修复思路

推荐修复方式是“规范化路径 + 目录边界判断 + 业务权限校验”组合使用,而不是依赖单一字符串过滤。

public String readUserFileSafe(Long userId, String fileName) throws IOException {
    Path basePath = Paths.get(uploadRoot, String.valueOf(userId))
            .toRealPath(LinkOption.NOFOLLOW_LINKS);

    Path targetPath = basePath.resolve(fileName)
            .normalize();

    if (!targetPath.startsWith(basePath)) {
        throw new SecurityException("invalid file path");
    }

    if (!Files.isRegularFile(targetPath, LinkOption.NOFOLLOW_LINKS)) {
        throw new FileNotFoundException("file not found");
    }

    return Files.readString(targetPath, StandardCharsets.UTF_8);
}

这段代码仍需要结合业务实际继续加固:

  • 如果允许预览的文件类型有限,应增加扩展名或 MIME 白名单;
  • 如果文件记录在数据库中,优先使用 fileId 查询真实存储路径,而不是直接信任文件名;
  • 对下载、预览、删除等操作都要校验文件归属;
  • 避免把服务端物理路径返回给前端;
  • 对异常响应做统一处理,避免泄露堆栈和绝对路径。

代码审计中的几个注意点

1. 不要只搜危险函数

很多项目会封装文件操作工具类,例如 StorageService、FileManager、OssHelper。直接搜索 FileInputStream 可能搜不到真实入口。建议同时搜索业务关键字:

preview
readFile
download
export
upload
fileName
path
storage
attachment

2. 注意框架自动绑定

Spring MVC 中 @RequestParam、@PathVariable、@RequestBody 都可能引入外部输入。尤其是复杂对象绑定时,危险字段可能藏在 DTO 内部,例如 path、template、callbackUrl、sort、filter 等。

3. 关注二次传递

有些参数不是当前接口直接使用,而是写入数据库、缓存或消息队列,随后被异步任务读取。审计这类问题时,要把“写入点”和“消费点”连起来看,否则容易漏掉存储型风险。

4. 区分漏洞和不可利用缺陷

如果某个危险函数的输入完全来自后端固定配置,且没有被外部参数影响,通常不能直接定性为漏洞。审计报告中要说明可控性、触发条件、影响范围和修复建议,避免只给结论。

辅助工具实践

静态分析工具能提高覆盖率,但不应替代人工判断。日常可以组合使用 grep、IDE 调用链、Semgrep 或 CodeQL。下面是一个简单的 Semgrep 规则示例,用于辅助发现 Java 中用户参数进入文件读取的可疑模式。

rules:
  - id: java-file-read-from-request-param
    languages: [java]
    message: Request parameter may flow into file read operation
    severity: WARNING
    patterns:
      - pattern-either:
          - pattern: |
              $T $V = $REQ.getParameter($P);
              ...
              new FileInputStream($V);
          - pattern: |
              @RequestParam $TYPE $V
              ...
              Files.readString(... $V ...);

这类规则只能发现一部分浅层问题。真实项目中变量可能经过多次赋值、对象封装、方法返回,仍需要人工沿调用链确认。

风险评级建议

判断维度关注点
可控性参数是否由外部用户直接或间接控制
边界突破是否能访问授权目录外的资源
权限影响是否能读取其他用户或系统级文件
数据敏感性是否涉及配置、密钥、用户数据、业务凭据
触发门槛是否需要登录、特定角色或复杂前置条件

如果只能读取当前用户自己上传的普通文件,风险相对有限;如果能越权读取其他用户文件或服务端配置,风险应明显上调。评级最好结合实际影响,不要只按漏洞类型机械套级别。

总结

代码审计的关键不是记住多少危险函数,而是建立稳定的数据流追踪习惯:入口是否可控,过程中是否被正确处理,最终是否进入敏感操作。对于文件读取这类常见功能,建议重点检查路径规范化、目录边界、文件归属和异常回显。

从长期维护角度看,修复不应停留在某个接口的临时过滤。更好的方式是把文件访问能力收敛到统一服务层,统一做路径解析、权限校验、日志记录和异常处理。这样既减少重复代码,也能降低后续功能迭代带来的回归风险。

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

0篇意见

推荐意见

没有意见。

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