背景
很多漏洞不是靠“扫一下”就能稳定发现的,尤其是业务系统里的鉴权缺失、越权、文件处理、模板渲染等问题。工具能提高覆盖率,但最终还是要回到代码路径和数据流本身。这里整理一套我在审计 Web 项目时常用的流程,偏长期可复用的方法,不针对某个具体系统,也不包含真实目标攻击操作。
文章里的示例以常见 Java/Spring Web 项目为主,思路同样适用于 PHP、Node.js、Python Web 框架。重点是如何把“看代码”变成一套可复现、可记录、可验证的工作流。
审计前的准备
拿到代码后,第一步不是直接搜索危险函数,而是先把项目跑起来或至少把结构理清。建议先关注以下信息:
- 框架类型:Spring MVC、Spring Boot、ThinkPHP、Express、Django 等。
- 路由入口:Controller、Router、Servlet、Filter、Interceptor。
- 认证方式:Session、JWT、OAuth2、自研 Token、网关注入用户信息。
- 数据访问层:MyBatis、JPA、原生 SQL、ORM Query Builder。
- 文件处理:上传、下载、导入导出、压缩解压、图片处理。
- 外部调用:HTTP Client、消息队列、对象存储、命令执行、脚本引擎。
如果项目依赖较多,先生成依赖树,有助于判断是否存在已知高风险组件。
mvn dependency:tree -DskipTests
# Gradle 项目
gradle dependencies
# Node.js 项目
npm ls --depth=0
# Python 项目
pip freeze这一步的目的不是立刻下结论,而是建立“攻击面地图”。后续所有问题都应能落回到某个入口、某条数据流、某个权限边界。
入口面梳理:先看谁能被访问
Web 审计里,入口面通常比危险函数更重要。很多严重问题并不是函数本身危险,而是它被未授权用户访问到了。
以 Spring 项目为例,可以先搜索 Controller 和路由注解:
grep -R "@RequestMapping\|@GetMapping\|@PostMapping\|@PutMapping\|@DeleteMapping" -n src/main/java建议把接口整理成表格,至少包含路径、方法、参数、是否登录、角色要求、关键业务动作。
| 字段 | 说明 |
|---|---|
| URL | 接口路径,包含类级别和方法级别路由 |
| Method | GET、POST、PUT、DELETE 等 |
| Auth | 是否需要登录,是否只依赖前端控制 |
| Role | 是否有角色、租户、组织维度校验 |
| Action | 查询、修改、删除、导入、导出、审批等 |
如果系统使用拦截器或网关鉴权,需要继续确认白名单配置。例如:
grep -R "excludePathPatterns\|permitAll\|anonymous\|whiteList\|ignoreUrls" -n src/main/java src/main/resources常见问题包括:管理接口被加入白名单、仅前端隐藏按钮但后端未校验、同一 Controller 中部分接口缺少注解、测试接口未下线。
鉴权与越权:看权限是否贴近资源
越权问题的核心不是“有没有登录”,而是“当前用户是否有权访问这个资源”。代码审计时建议重点检查以下模式:
- 接口只校验登录态,不校验资源归属。
- 通过 userId、orgId、tenantId、deptId 等参数直接查询数据。
- 权限判断依赖前端传入的角色或用户字段。
- 批量操作只校验了部分 ID,未逐条校验。
- 管理员和普通用户共用 Service,但 Controller 层校验不一致。
一个典型的风险代码形态如下:
// 示例:仅用于说明风险模式
@GetMapping("/order/detail")
public Order detail(Long orderId) {
return orderService.getById(orderId);
}更合理的做法是把资源归属校验放在服务端可信上下文里,而不是依赖请求参数。
@GetMapping("/order/detail")
public Order detail(Long orderId) {
Long currentUserId = SecurityContext.getCurrentUserId();
return orderService.getByIdAndUserId(orderId, currentUserId);
}多租户系统还要额外关注 tenantId。很多系统登录态里有租户信息,但查询条件忘记带租户约束,最终形成跨租户数据访问。
SQL 注入:不要只搜字符串拼接
SQL 注入审计常见误区是只搜索 Statement 或 "select " +。实际项目里,MyBatis XML、动态排序、动态表名、报表查询更容易出问题。
可以优先搜索这些关键点:
grep -R "\${" -n src/main/resources src/main/java
grep -R "order by\|group by\|limit\|tableName\|columnName" -ni src/main/resources src/main/java在 MyBatis 中,#{} 通常会走预编译参数绑定,而 ${} 是字符串替换。如果 ${} 接收用户可控输入,就需要重点确认白名单限制。
<!-- 风险示例:sortField 如果来自请求参数,需要白名单校验 -->
ORDER BY ${sortField} ${sortOrder}更稳妥的方式是在后端把可排序字段映射成固定枚举:
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"createdTime", "created_time",
"amount", "amount"
);
String column = SORT_FIELD_MAP.getOrDefault(req.getSortField(), "created_time");这里不是让所有动态 SQL 都消失,而是要求“结构性 SQL 片段”必须来自可信白名单,不能直接由用户输入决定。
文件上传与下载:重点看路径和内容处理
文件相关漏洞经常出现在上传头像、导入 Excel、导出报表、下载附件、压缩包解压等功能里。审计时建议分成两条线:文件内容和文件路径。
上传功能重点看:
- 扩展名校验是否可被大小写、双扩展名、特殊字符影响。
- 是否校验 MIME 和文件魔数,而不是只信任 Content-Type。
- 上传目录是否在 Web 可执行目录下。
- 文件名是否由服务端重命名,是否保留用户传入路径片段。
- 图片处理库、文档解析库是否存在历史漏洞或解析风险。
下载功能重点看路径穿越风险。典型风险代码如下:
// 示例:不要直接拼接用户传入文件名
Path path = Paths.get(baseDir + "/" + request.getParameter("file"));建议使用规范化路径并确认最终路径仍位于允许目录下:
Path base = Paths.get("/data/files").toRealPath();
Path target = base.resolve(fileName).normalize();
if (!target.startsWith(base)) {
throw new SecurityException("invalid file path");
}如果存在压缩包解压,需要额外检查 Zip Slip。不要相信压缩包内的文件名,解压每个条目前都应做路径归一化校验。
SSRF 与外部请求:关注 URL 可控点
现代业务里服务端主动发起 HTTP 请求很常见,比如图片抓取、Webhook、URL 预览、文件转存、第三方回调验证等。审计时可以搜索常见 HTTP 客户端:
grep -R "RestTemplate\|HttpClient\|OkHttp\|URLConnection\|WebClient" -n src/main/java如果 URL、host、redirect 地址部分可控,需要检查是否有 SSRF 防护。比较可靠的思路包括:
- 只允许访问固定业务域名或白名单域名。
- 禁止访问内网地址、回环地址、链路本地地址和云元数据地址。
- 解析域名后校验 IP,并在连接阶段避免 DNS Rebinding 影响。
- 限制协议,只允许 http/https。
- 限制跳转次数,并对跳转后的地址重新校验。
- 设置连接超时、读取超时和响应大小限制。
仅用字符串判断 url.startsWith("http") 或黑名单关键字过滤,一般不够稳。SSRF 防护要放在“解析后”的地址对象和 IP 结果上做判断。
反序列化与表达式注入:从数据边界入手
反序列化问题并不只存在于传统 Java 原生序列化。JSON 多态反序列化、XML 解析、模板引擎、表达式语言、规则引擎,都属于需要关注的数据边界。
可以优先搜索这些关键点:
grep -R "readObject\|ObjectInputStream\|enableDefaultTyping\|@JsonTypeInfo" -n src/main/java
grep -R "SpEL\|ExpressionParser\|eval\|ScriptEngine\|freemarker\|velocity" -ni src/main/java src/main/resources审计时关注两个问题:
- 输入是否来自不可信用户,例如请求体、消息队列、Redis、数据库中可被低权限用户写入的字段。
- 执行上下文是否具备危险能力,例如类加载、方法调用、文件读取、命令执行。
对于 JSON 多态反序列化,建议关闭不必要的默认类型支持,或使用明确的类型白名单。对于表达式和模板功能,尽量避免让用户直接控制模板内容;如果业务必须支持,应使用沙箱和能力限制。
日志与敏感信息:低成本但很常见
日志泄露容易被忽略,但在事件响应里经常能看到它的影响。审计时可以搜索以下字段:
grep -R "password\|passwd\|token\|secret\|accessKey\|privateKey\|Authorization" -ni src/main/java src/main/resources重点检查:
- 登录失败日志是否打印明文密码。
- 异常栈是否把 Token、Cookie、请求体完整打出。
- 配置文件是否提交了生产密钥。
- 导出功能是否包含身份证、手机号、银行卡等敏感字段。
- 接口响应是否返回过多用户对象字段。
建议在统一日志切面或网关层做敏感字段脱敏,避免依赖每个业务开发手工处理。
验证方式:以最小影响证明风险
代码审计发现疑似问题后,需要验证,但验证要遵循最小影响原则。建议使用本地环境、测试环境或授权范围内的靶场数据,不要在生产系统上做破坏性测试。
一个比较稳妥的验证记录应包含:
- 受影响接口或代码位置。
- 前置条件,例如是否需要登录、需要什么角色。
- 可控参数和数据流向。
- 风险结果,例如越权读取、未授权导出、路径穿越读取测试文件。
- 影响范围评估。
- 修复建议和回归验证方式。
对于可能造成数据修改或服务异常的问题,尽量构造只读验证。例如越权问题可以验证是否返回了不属于当前用户的测试数据;文件下载问题可以使用专门放置的无敏感测试文件;SSRF 可以在本地受控 HTTP 服务上观察请求。
常用辅助脚本:提高覆盖率,不替代判断
一些简单命令能帮助快速定位风险点,但不要把搜索结果直接等同于漏洞。下面是我常用的初筛方式:
# 查找潜在命令执行点
grep -R "Runtime.getRuntime\|ProcessBuilder\|exec(" -n src/main/java
# 查找潜在文件操作点
grep -R "FileInputStream\|FileOutputStream\|Files\.\|Paths\.get" -n src/main/java
# 查找潜在重定向点
grep -R "redirect:\|sendRedirect\|Location" -n src/main/java src/main/resources
# 查找潜在 CORS 配置问题
grep -R "allowedOrigins\|allowCredentials\|CorsConfiguration" -n src/main/java这些命令更适合做“线索发现”。真正判断风险,还要结合调用链、参数来源、权限上下文和部署环境。
修复建议的写法
高质量审计报告不应该只写“存在漏洞,建议过滤”。更可执行的修复建议应具体到控制点,例如:
- 鉴权问题:在 Service 层增加基于当前登录用户的资源归属校验,避免只在前端或 Controller 层控制。
- SQL 注入:参数值使用预编译绑定;字段名、排序方向、表名等结构性片段使用服务端白名单映射。
- 文件下载:使用固定根目录、路径规范化和 startsWith 校验;下载文件 ID 映射到服务端存储路径。
- SSRF:使用域名/IP 白名单、解析后 IP 校验、禁止内网地址、限制跳转和响应大小。
- 敏感信息:统一脱敏日志,清理配置中的密钥,响应 DTO 避免直接返回数据库实体。
修复后还需要回归验证。尤其是鉴权和多租户问题,要补充自动化测试用例,否则后续改动很容易再次引入。
注意点
- 不要只看单个函数,要看完整调用链。很多风险在 Controller 看不出来,在 Service 或 Mapper 才暴露。
- 不要默认网关已经处理所有权限。网关通常适合做粗粒度认证,资源级授权仍应在业务侧完成。
- 不要把“内网系统”当作安全边界。内网账号滥用、钓鱼入口、供应链依赖都会让内网接口暴露风险。
- 不要忽略异步任务和定时任务。消息队列、导入任务、回调处理经常绕过常规鉴权链路。
- 不要用破坏性方式验证漏洞。审计的目标是确认风险并推动修复,不是制造额外事故。
总结
有效的 Web 代码审计,核心是把入口、权限、数据流和危险操作串起来看。工具和关键词搜索能提高效率,但不能替代对业务边界的理解。
一套可复用的流程可以概括为:先梳理入口面,再确认鉴权模型,然后围绕 SQL、文件、外部请求、反序列化、日志等高风险模块做数据流分析,最后用最小影响方式验证并给出可落地修复建议。
长期来看,审计过程中沉淀接口清单、权限矩阵、危险函数索引和回归用例,比单次发现几个问题更有价值。这样下次版本迭代时,安全检查就不会从零开始。
推荐意见