跳转到帖子

背景

很多内网 Web 系统上线时间早、业务依赖重,平时很少有人系统性审计。真正出问题时,常见情况不是单点漏洞特别复杂,而是几个普通风险叠加:老旧框架、调试接口遗留、上传目录可访问、权限校验分散、日志缺失。本文整理一次偏防守视角的代码审计与风险收敛方法,重点放在如何发现问题、验证风险边界、推动修复,不涉及未授权攻击或恶意利用。

场景假设是企业内部自研 Web 系统,技术栈为 Java Spring MVC / Spring Boot 混合,前端有少量老 JSP,部署在 Linux + Nginx + Tomcat 环境。目标不是“打穿系统”,而是通过代码审计和受控验证,把可被利用的缺陷收敛到可修复、可复测的清单。

问题与场景

这类系统常见的问题并不新,但因为业务迭代多年,风险点往往藏在边角位置:

  • 接口权限校验不一致:部分 Controller 依赖前端菜单控制,没有后端鉴权。
  • 文件上传处理粗糙:只校验后缀或 Content-Type,上传目录又能被直接访问。
  • SQL 拼接遗留:新模块使用 ORM,老模块仍有字符串拼接 SQL。
  • 配置暴露:测试配置、默认口令、调试开关混入生产分支。
  • 日志与审计不足:关键操作没有记录操作者、对象、结果,事件复盘困难。

审计时建议先不要急着跑工具扫一遍就结束。工具能帮忙发现一部分明显问题,但对业务权限、对象越权、流程缺陷的覆盖有限。更稳妥的方式是先梳理入口,再按风险类型分层排查。

审计前的准备

代码审计前我通常会先整理三类信息:部署拓扑、路由入口、敏感功能。这样后续验证会更有边界,不容易误伤业务。

  • 部署拓扑:Nginx 转发规则、Tomcat context、静态目录、上传目录、定时任务。
  • 路由入口:Controller、Filter、Interceptor、Servlet、JSP 直连页面。
  • 敏感功能:登录、用户管理、角色权限、文件上传下载、导入导出、报表查询、系统配置。

可以先用简单命令对代码结构做一个粗略索引:

find . -type f \( -name "*.java" -o -name "*.jsp" -o -name "*.xml" -o -name "*.properties" -o -name "*.yml" \) | wc -l

grep -RIn "@RequestMapping\|@GetMapping\|@PostMapping" src/ | head -n 50

grep -RIn "upload\|download\|export\|import\|FileInputStream\|FileOutputStream" src/

如果是 Spring Boot 项目,可以重点关注以下位置:

  • 安全配置:SecurityConfig、WebMvcConfigurer、Interceptor 注册逻辑。
  • 统一异常:是否吞掉异常,是否把堆栈直接返回前端。
  • 配置文件:application-prod.yml、bootstrap.yml、logback 配置。
  • 依赖版本:pom.xml / build.gradle 中是否存在长期未升级组件。

技术分析:几个高频风险点

1. 权限校验分散导致的接口越权

很多老系统会在页面层控制菜单显示,但后端接口本身没有强制鉴权。审计时不能只看登录拦截器是否存在,还要看“登录态”和“操作权限”是否分开校验。

一个比较危险的模式是只判断 session 中是否存在用户:

if (session.getAttribute("user") == null) {
    return "redirect:/login";
}

这种做法只能说明用户已登录,不能说明用户有权限访问某个接口。更合理的方式是让权限校验集中在 Filter、Interceptor 或安全框架中,并且权限点绑定到后端路由,而不是只绑定前端菜单。

排查时可以搜索:

grep -RIn "getAttribute(\"user\"\)\|hasPermission\|checkPermission\|@PreAuthorize\|@RequiresPermissions" src/

如果同一个项目里既有 @PreAuthorize,又有手写 session 判断,还存在完全没有注解的 Controller,就要重点抽样验证。验证时应在授权测试环境中使用不同角色账号对比访问结果,避免对生产环境做破坏性操作。

2. 文件上传与可访问目录叠加

上传漏洞的风险不一定来自“上传后执行”,也可能来自敏感文件落地、覆盖文件、路径穿越、恶意内容被二次解析等。审计重点包括:

  • 是否只依赖文件名后缀判断。
  • 是否使用用户传入的原始文件名直接落盘。
  • 上传目录是否在 Web 根目录下。
  • 下载接口是否存在任意文件读取风险。
  • 是否对文件大小、类型、数量做限制。

常见的不安全写法如下:

String fileName = multipartFile.getOriginalFilename();
File dest = new File(uploadPath + "/" + fileName);
multipartFile.transferTo(dest);

至少应做这些处理:生成服务端文件名、白名单扩展名、限制大小、校验真实文件类型、目录规范化、上传目录禁止执行、下载时按文件 ID 映射真实路径。

Path baseDir = Paths.get(uploadPath).toAbsolutePath().normalize();
String safeName = UUID.randomUUID().toString() + ".pdf";
Path target = baseDir.resolve(safeName).normalize();

if (!target.startsWith(baseDir)) {
    throw new IllegalArgumentException("invalid path");
}

Nginx 层也建议明确限制上传目录的执行能力,不要让上传目录被脚本引擎处理。示例仅表达思路,具体规则要结合实际路径调整:

location /uploads/ {
    alias /data/app/uploads/;
    autoindex off;
    default_type application/octet-stream;
    add_header X-Content-Type-Options nosniff;
}

3. SQL 拼接与动态查询

老系统里最常见的是“新代码用 MyBatis 参数绑定,老代码直接拼接条件”。尤其是报表、导出、综合查询页面,字段多、条件多,很容易出现拼接 SQL。

String sql = "select * from orders where 1=1";
if (StringUtils.isNotBlank(keyword)) {
    sql += " and customer_name like '%" + keyword + "%'";
}

审计时可以搜索以下关键词:

grep -RIn "Statement\|createQuery\|createNativeQuery\|\$\{\|order by \|like '%" src/

需要注意,MyBatis 中 ${} 不等于 #{}。前者是字符串替换,后者是参数绑定。对于排序字段这类无法直接绑定的场景,应使用白名单映射,而不是把用户输入直接拼进 SQL。

Map<String, String> allowSort = new HashMap<>();
allowSort.put("time", "create_time");
allowSort.put("amount", "total_amount");

String sortColumn = allowSort.getOrDefault(req.getSort(), "create_time");

4. 反序列化与表达式注入的边界排查

Java 项目如果引入过老版本组件,反序列化和表达式注入需要单独看。这里不建议在业务环境做危险 payload 验证,更适合通过依赖分析、调用链确认、受控单元测试来判断风险。

可以先看依赖树:

mvn dependency:tree | egrep "commons-collections|fastjson|jackson-databind|xstream|shiro|spring-expression"

关注点包括:

  • 是否存在历史高危版本组件。
  • 是否有对外暴露的反序列化入口,例如直接接收 ObjectInputStream、XMLDecoder。
  • 是否启用了危险的类型自动识别能力。
  • 表达式是否包含用户可控输入。

判断这类风险时,不要只凭“存在某个依赖”就下结论。更准确的结论应包含:组件版本、是否可达、入口在哪里、用户输入能否控制关键参数、现有防护是否生效。

5. 配置与日志问题

配置泄露和日志缺失经常被低估。比如测试环境回调地址、默认管理口令、对象存储密钥、数据库连接串被提交到仓库。审计时可以对配置文件做一次敏感信息扫描:

grep -RIn "password\|passwd\|secret\|accessKey\|token\|jdbc:\|redis" . \
  --exclude-dir=target --exclude-dir=.git

日志方面,建议至少覆盖这些事件:

  • 登录成功、失败、锁定、退出。
  • 新增、删除、修改用户与角色。
  • 导入、导出、下载敏感数据。
  • 系统配置变更。
  • 权限拒绝与异常访问。

日志要避免记录明文密码、完整 token、身份证号等敏感数据。更推荐记录脱敏后的对象标识、操作者 ID、来源 IP、请求 ID、结果状态,便于后续排查。

关键步骤:从发现到修复闭环

实际项目里,审计报告如果只列“存在漏洞”,研发往往难以处理。更有效的方式是把每个问题写成可复现、可定位、可修复、可复测的闭环。

阶段重点产出
资产梳理确认代码分支、部署环境、入口接口接口清单、敏感功能列表
静态审计定位高风险函数、配置、依赖疑似风险点与代码位置
受控验证在授权测试环境验证影响复现步骤、请求样例、影响范围
修复建议给出最小改动与长期方案修复 PR 或整改说明
复测确认确认漏洞不可复现,避免引入回归复测记录、回归用例

一个比较实用的问题记录模板如下:

问题名称:接口权限校验缺失
影响模块:/admin/user/export
代码位置:UserController.java:128
触发条件:普通登录用户可直接请求导出接口
影响说明:可能导致非授权用户导出用户列表
验证环境:测试环境,账号 role=test_user
修复建议:后端增加权限点校验,导出操作纳入审计日志
复测标准:无权限用户访问返回 403,并记录权限拒绝日志

注意点

  • 不要在生产环境做破坏性验证,尤其是文件写入、批量导出、压力型请求。
  • 验证权限问题时,应使用授权测试账号,避免使用真实高权限账号做无关操作。
  • 报告里尽量避免夸大影响,能确认到哪里就写到哪里。
  • 修复建议要区分短期止血和长期治理,例如先加权限校验,再统一权限框架。
  • 依赖漏洞不要只看 CVE 列表,要确认组件是否可达、功能是否启用。
  • 涉及敏感数据的截图、日志、请求包应脱敏后再流转。

总结

内网 Web 资产的安全问题往往不是单个“高深漏洞”,而是长期迭代中留下的工程缺口。一次有效的代码审计,核心不是堆工具结果,而是围绕入口、权限、数据、文件、配置、日志建立清晰判断链。

我的经验是,最值得长期投入的是三件事:权限校验集中化、文件与数据访问最小化、关键操作可审计。把这些基础能力做好,很多常见风险会自然下降。审计报告也应尽量服务于修复闭环,让研发能按代码位置和复测标准逐项处理,这比单纯列风险更有价值。

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

0篇意见

推荐意见

没有意见。

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