刚接手一个紧急任务:一个上线两年的老系统被业务方投诉接口响应异常,让我"顺手看一眼"。结果十分钟内,我在一个导出功能里发现了一个垂直越权漏洞——普通用户只要改一下URL里的ID,就能导出别人的订单数据。那一刻我意识到,安全审计这项技能的关键不在于你会用多少工具,而在于你能不能在最短时间内建立起一套"输入-处理-输出"的全局视角,然后顺着数据流找到那个没有校验的落点。
这篇文章想聊聊 security-audit-skill 这套技能本身:它包含哪些能力、审计时按什么路径推进、高频漏洞往哪儿看、工具怎么用才不背锅,以及报告怎么写才能推动修复。我自己在甲方安全团队、乙方审计项目里都待过,踩过不少坑,下面这些内容都是实际干活时沉淀下来的思路,适合刚转行做安全审计、或者想系统提升代码审计能力的同学参考。
1. 安全审计的边界认知:它和渗透测试不是一回事
很多刚入门的人会把安全审计和渗透测试混在一起,这其实是两套完全不同的工作逻辑。渗透测试是在一个运行中的系统上做黑盒验证,你看到的是一堆接口和页面,目标是"能不能打进去";而安全审计面对的是源代码、配置文件、架构文档,你的目标变成了"这个系统里面有没有可以被利用的缺陷"。一个从外部往里打,一个从内部往外看,思维方式截然不同。
1.1 四种常见的审计类型
根据审计对象和阶段的不同,我把日常碰到的审计需求拆成四类:
| 审计类型 | 审计对象 | 产出形式 | 常见手段 |
|---|---|---|---|
| 代码审计 | 应用源代码 | 漏洞清单、修复建议 | 静态扫描、人工复核、污点追踪 |
| 配置审计 | 部署脚本、中间件、云资源策略 | 配置风险报告 | 基线比对、策略检查 |
| 架构审计 | 系统架构图、接口设计、权限模型 | 架构风险评估 | 威胁建模、数据流分析 |
| 供应链审计 | 第三方依赖、开源组件 | 依赖风险清单 | SCAT扫描、许可证核查 |
这四类审计对技能的要求很不一样。代码审计需要扎实的编程功底和漏洞模式积累,配置审计考验你对中间件和云平台的理解,架构审计更需要威胁建模能力,供应链审计则依赖对开源生态的熟悉程度。一个成熟的安全审计人员,通常是以某一类为主,其他几类能看懂、能配合,不太可能样样精通。
1.2 审计思维的两个常见误区
第一个误区是"审出漏洞就完事了"。实际工作中,漏洞描述里如果缺少完整的调用链和触发条件,研发几乎不会认账。审计的真正产出是"证据链"——从入口参数、处理逻辑到风险落点,每一步都要能对上,这才叫有效的审计结论。
第二个误区是"审得越多越厉害"。我曾经见过一个审计报告列了300多个问题,其中280个是工具扫出来的低风险告警。这种报告对修复排期毫无参考价值,反而会淹没真正的高危问题。好的审计是在保证覆盖率的前提下,把有限精力集中在可被利用、影响范围明确的缺陷上。说白了,审计服务于风险决策,不服务于漏洞数量排名。
2. 审计前必须扎根的技术功底:读得懂代码,才讲得出危害
安全审计看起来很依赖工具,但工具只是放大器。真正的底层能力是你读代码的能力和对漏洞模式的敏感度。我招人的时候有一个不太科学的判断标准:让候选人读一个他没见过的开源项目的核心模块,如果他在半小时内能说清楚这个模块的数据流和最关键的风险点,基础就过关了。
2.1 语言能力的最低门槛
做Java项目的审计,至少要能看懂Spring MVC的请求处理链路、MyBatis的SQL执行过程、Dubbo或者Feign的调用方式,不然你连入口都找不到。做好前端代码审计,至少要能梳理出API调用关系和鉴权token的存储方式。
说到底,审计用的语言能力不是"能写业务代码",而是"能读懂框架源码"。这两者之间差着一个距离:写业务代码时你只关心功能实现,而审计时你需要理解框架帮你做了什么、没帮你做什么。比如Spring Security配了拦截器,不代表每个接口都有权限控制;MyBatis的${}和#{}看着差不多,审计时就得逐处确认。
2.2 漏洞模式库的构建方法
审计效率高的人,脑子里通常装着一张"漏洞模式表":
- 输入处理类:未校验参数直接进入SQL、命令、路径、模板引擎
- 认证授权类:仅前端控制权限、ID可预测且后端不校验属主
- 数据保护类:敏感信息打印日志、明文存储口令、接口返回多余字段
- 执行环境类:反序列化入口、SpEL表达式注入、动态类加载
这张表的来源不是书本,而是CVE漏洞库和实际审计案例。我自己的习惯是每复现一个漏洞,就把它抽象成"触发条件+审计特征"两条,记录到一个私有笔记里。比如某个CVE的根因是用户输入进了Type.parseType(),我就在笔记里写一行"搜索特征:parseType、invoke、getMethod,注意入参是否可控"。积累多了以后,拿到新代码会自动浮现相似模式。
2.3 数据流视角:画不出图就谈不上审计
代码审计最核心的思维是数据流思维。任何漏洞的本质都是"不可信输入到达了危险操作"。审计的时候,我会在草稿纸上画一张简化的数据流图:
外部入口(HTTP参数、文件上传、消息队列、定时任务触发) ↓ 处理逻辑(解码、反序列化、类型转换、SQL拼接) ↓ 风险落点(数据库查询、命令执行、文件写入、HTTP请求外发)画图的过程就是审计的思考过程。入口有哪些?每个入口的数据到了哪里?中间经过了哪些加工?每一层有没有做校验?这张图画完,高危点基本就浮出水面了。没有这张图,你就是在代码里大海捞针。
3. 一次完整审计的推进路径:从拿代码到出报告的七个步骤
审计没有统一的标准化流程,但我做了多年下来,已经形成一套固定的推进节奏。不管是审一个微服务还是审一个大型单体应用,只要按这个路径走,基本能做到既覆盖全面又不漏重点。
3.1 第一步:信息收集和架构摸查
拿到代码的第一件事不是急着看业务逻辑,而是先看项目结构和启动配置。重点关注这些文件和目录:
README、架构文档、接口文档pom.xml、package.json、requirements.txt等依赖清单application.yml、.env、配置中心相关类- migration脚本和数据库表结构
- Dockerfile、K8s部署文件、CI流水线配置
这些文件能在半小时内告诉你:这个系统用了什么框架、哪些第三方组件、有没有外部依赖、配置项是否硬编码了敏感信息。曾经有个项目,我就是在.env文件里直接看到了数据库明文密码和云存储密钥,这比挖代码漏洞严重得多。
3.2 第二步:建立入口清单
很多审计新手栽在"不知道代码从哪儿看起"。我的做法是先建一张入口清单:
- HTTP接口:Controller、Router、ViewSet等文件
- 消息队列消费者:监听方法、事件处理器
- 定时任务:Task、Job、Cron方法
- 文件导入导出:上传接口、导出接口、批量处理
- 外部系统回调:Webhook、Callback
入口清单决定了审计覆盖率。漏掉一个回调接口,等于漏掉一块攻击面。所以这一步宁可多列,不可少列。我一般会在IDE里建一个书签分组,把入口文件全部标记出来,后面逐条过。
3.3 第三步:危险函数全量搜索
入口清单建好后,下一步是全局搜危险函数。这一步追求的是"全",把代码库里所有可能出现风险的位置列出来。常用的搜索模式包括:
# SQL操作 jdbcTemplate、MyBatis Mapper、createNativeQuery、executeQuery # 命令执行 Runtime.getRuntime().exec、ProcessBuilder、system、os.system # 文件操作 new File、FileInputStream、FileOutputStream、Path.of、getCanonicalPath # 反序列化 readObject、ObjectInputStream、pickle.loads、yaml.load # 模板/表达式注入 Thymeleaf、freemarker、Velocity、SpelExpressionParser、eval搜出来的命中点,我会按"是否跟入口数据有关"来分级:如果命中点的数据来源能追溯到请求参数,标红;来源是配置项或常量,标黄;完全无关,跳过。这一步能把审计范围从几万行代码收敛到几十个关键点。
3.4 第四步:污点路径追踪
危险函数命中之后,就要开始真正的核心工作——追踪数据从入口到这个函数的完整路径。追踪过程中要回答三个问题:
- 数据有没有经过校验?校验的强度是否足够?
- 中间有没有编码、转义、过滤操作?这些操作在目标场景下是否有效?
- 有没有前置条件才能到达这里?比如需要登录用户、需要特定角色?
举个例子,一个MyBatis的${}查询命中点,如果数据从请求的sort字段直接拼接进来,同时前面只有一个简单的正则校验,那就是一个SQL注入问题。但这个注入是利用难度还是直接注入,又要看SQL语句的结构、数据库类型、能否闭合,这些细节决定了危害等级。
3.5 第五步:业务逻辑专项核查
技术型漏洞用危险函数搜索能覆盖大部分,但业务逻辑漏洞必须靠人肉看代码。越权、批量注册、验证码绕过、金额篡改这类问题,没有固定的函数特征,只能沿着业务链路一个一个去捋。
越权是我的重点核查项。审计时我会盯住所有按ID查询详情的接口,看两点:接口有没有做登录态校验,有没有做资源属主校验。前者依赖通用的鉴权框架,后者依赖业务代码里的判断逻辑。很多项目的鉴权框架配得好好的,但程序员写接口时忘了在Service层再校验一次资源归属,于是平行越权就出现了。
3.6 第六步:工具扫描与人工复核
工具在这个阶段进场。我会先跑一遍Semgrep规则集、Gitleaks密钥扫描和依赖库漏洞扫描,然后把工具输出和前面人工追踪的结果合并去重。工具的命中结果我从来不直接采信,一律回到代码里看上下文。原因很简单:静态分析天然存在大量误报,没有上下文判断,你根本分不清某个告警是真漏洞还是代码风格问题。
3.7 第七步:验证与写报告
评估为高危的漏洞,我会在本地或者测试环境做最小验证。验证的原则是"宁轻勿重"——只证明漏洞存在即可,不做过度的利用,更不会碰生产环境。验证完成后,把"入口参数+触发链路+风险落点+修复建议"整理进报告。这个环节我最看重可复现性,研发拿到报告后应该能按图索骥,一步不差地复现问题。
4. 高频漏洞往哪儿看:六类问题的审计切入点
不同项目的业务差异很大,但高频漏洞的落点就那些地方。我在审计时,对每一类问题都有固定的切入点,按着这个思路查,命中率会高很多。
4.1 SQL注入:重点盯拼接和${}
在Java场景中,MyBatis的${}直接拼接、原生JDBC的字符串拼接、JPA的原生查询都是高危点。审计时的固定动作是:
- 全局搜
${,逐一确认包在里面的字段是否来自外部 - 搜
createNativeQuery、createQuery,看参数怎么进入 - 看ORM框架是否开启了
AllowSqlInjection之类的兼容选项
// 高危示例:sort字段直接拼接,排序参数未做白名单校验 String sql = "SELECT * FROM orders ORDER BY " + sortField; Query query = entityManager.createNativeQuery(sql);修复方案并不复杂:排序字段用白名单映射,其他参数一律用预编译占位符。审计报告里我会直接给出修好的代码示例,而不是只丢一句"建议使用预编译"。
4.2 越权访问:链路上缺了东西
越权问题的根源永远是"链路上某个环节漏了校验"。审计时我的固定动作是:
- 找出所有接收ID参数的接口
- 检查进入Controller之前有无统一的鉴权拦截
- 检查Service层是否按当前登录用户做了数据过滤
- 重点看导出、下载、详情、编辑、删除这五类接口
// 典型水平越权:直接按taskId查询,没有校验taskId是否属于当前用户 @GetMapping("/task/detail") public Result<Task> detail(@RequestParam Long taskId) { return Result.ok(taskService.getById(taskId)); }这种问题的修复核心不只是在接口层加校验,而应该把"数据归属判断"下沉到Service层,确保所有调用方都不会绕过。审计时要看功能入口多不多,如果同一个Service方法被五个接口调用,只修一个接口是远远不够的。
4.3 文件上传与路径穿越:文件名和路径是重灾区
文件上传需要检查点包括:后缀名校验能不能绕过(比如用大小写、双扩展名、空字节)、文件内容是否真正校验(比如只查了Content-Type头)、上传文件存储路径是否可控、访问路径是否有鉴权。路径穿越的重点是文件下载和导出功能,文件名拼接用户输入时容易出现../逃逸。
# 高危示例:filename来自请求参数,直接拼接文件路径 filepath = os.path.join(UPLOAD_DIR, request.args.get("filename")) return send_file(filepath)这类问题修复时要做到三层:文件名白名单、路径规范化后校验前缀、下载接口加鉴权。任何一层缺失,都可能被人利用。
4.4 命令注入与代码执行:找绕过点比找命中点更难
命令注入的直接命中点好找,麻烦的是那些经过编码、拼接、二次处理的入口。审计时除了搜Runtime.exec、ProcessBuilder、os.system,还要看有没有eval、Invoke-Expression、GroovyShell、动态编译等间接执行入口。从审计经验看,很多命令注入问题出现在运维自动化脚本、数据导入处理、模板渲染这类"看起来不像业务功能"的模块里。
4.5 SSRF:服务端请求必须锁定目标
SSRF的审计点是所有由服务端发起的外部请求。典型的入口包括:URL参数、头像上传、网页抓取、Webhook配置、图片代理等功能。审计时重点关注:
- 请求地址是否由用户控制
- 是否限制了协议(比如只能HTTP)
- 是否做了内网地址屏蔽(尤其是一些云环境里的169.254.169.254元数据服务地址)
- 重定向时是否重新校验
// 高危示例:url完全由用户提供,且未做任何内网地址限制 URL url = new URL(request.getParameter("target")); HttpURLConnection conn = (HttpURLConnection) url.openConnection();修复方案最有效的一招是"禁止开放传URL",如果业务确实需要,就用白名单域名列表,而不是靠黑名单过滤。
4.6 敏感信息泄露:从日志到前端都要过一遍
敏感信息问题分散在不少位置。审计时的固定检查项包括:
- 是否有接口返回了不需要的敏感字段(比如
SELECT *直接返回了密码哈希) - 异常栈信息是否直接抛给前端(信息泄露有助于攻击者探测内部结构)
- 日志里是否打印了request参数、SQL语句、token信息
- 前端打包产物里是否包含接口密钥、内部地址、注释掉的调试代码
这类问题的修复往往很简单,但它们最容易引起合规层面的风险,在金融、医疗类项目里尤其不能放过。
5. 工具链的正确体位:扫描引擎、规则意识和误报处置
工具在审计中到底该占多大比重?我的经验是:工具承担"覆盖率兜底"的角色,真正决定审计质量的是人对结果的处理。把工具当主力、人当辅助是很多审计项目翻车的根源。
5.1 常用审计工具的真实分工
市面上工具不少,但每款工具的定位差别很大。我日常使用的一套组合是这样的:
| 工具 | 定位 | 适用场景 | 需要注意的问题 |
|---|---|---|---|
| Semgrep | 自定义规则扫描 | 快速定位指定模式,适合代码库大、团队有规则维护能力 | 默认规则覆盖面有限,需要根据项目定制 |
| CodeQL | 深度查询引擎 | 需要做复杂数据流分析的高价值项目 | 学习成本高,构建数据库耗时 |
| SonarQube | 代码质量平台 | 持续集成中的日常扫描 | 漏洞规则偏通用,安全问题需要二次判定 |
| Gitleaks | 密钥扫描 | 扫描历史提交中的AK/SK、口令、token | 很多命中是测试凭据,需人工判断 |
| OWASP Dependency-Check | 依赖库漏洞 | 识别第三方组件已知CVE | 依赖误报较多,需要核对受影响版本范围 |
这个组合很朴素,但足够应对绝大多数项目。关键点在于:这些工具的输出必须流到同一个评审池子,由人工统一过一遍,而不是谁扫出问题就直接报给研发。
5.2 规则调优的上下文意识
Semgrep这类工具最大的价值是你可以按项目定制规则。我自己会针对常用框架维护一套审计规则集,扫描前先跑通用规则,再跑项目定制规则。
比如针对Spring Boot项目,我会加一条规则:凡是@RequestParam、@PathVariable变量直接进入String.format或者拼接进SQL语句,就报警。针对Node.js项目,我会加一条规则:凡是req.query、req.body进入child_process.exec就报警。定制规则的精髓是"描述出你们项目里真正的风险模式",而不是堆砌一堆行业通用规则。
5.3 误报的判定原则
工具报出来的问题,回到代码里看上下文,通常会遇到这几种情况:
- 真漏洞:入口可控,链路完整,没有有效防护——必须上报
- 可控但已有防护:入口和落点之间存在有效的过滤或转义——结合防护强度决定是否上报
- 不可控:数据源来自配置、常量或可信内部调用——降级为提示,不报给研发
- 框架自动处理:引擎/框架底层已经做了转义——核实后关闭对应规则,避免下次再报
我见过不少审计新人被工具告警淹没,每天都在追着一堆无效告警跑。处理原则就一条——以数据流是否可控为第一判据,链路不通,告警不成立。
5.4 让工具进入CI但不吵人
推动团队在CI流水线里集成静态安全扫描,是我认为最能长期体现审计价值的事情。但这里有个关键经验:别把全量规则集一股脑塞进流水线,否则一天几百个告警,开发跑一次构建就放弃了。
我的做法是分两档:
- 阻断档:只有高危以上的问题才拦住合并,规则集严格控制,保留高置信度的模式
- 提示档:中低危问题写进MR评论,不阻塞流程,但让开发者知道存在风险
这样做的好处是既不会影响交付效率,又能保持对风险的持续关注。比起一年做一次集中审计,这种融入研发流程的轻量扫描,实际上更能控制风险增量。
6. 报告写作与修复推动:审计价值落地的那一步
审计做得再深再透,报告写得不行,价值就出不来。我看到过太多"堆了一堆漏洞描述但研发看不懂、不认账"的审计报告。这里面的问题往往不是研发不重视安全,而是审计人员没有把问题讲清楚。
6.1 报告结构:结论前置,细节后置
一份合格的审计报告应该做到"领导看结论,开发看细节"。我惯用的结构是:
- 审计概述:范围、时间、人员、方法
- 风险统计:按高/中/低汇总问题数量,附一张概览表
- 高危摘要:每一条用两三句话讲清楚是什么问题、为什么严重
- 漏洞详情:按严重程度排序,逐条描述,附完整调用链路和修复建议
- 改进建议:从技术、流程、制度层面给出后续建议
有了这个结构,技术团队负责人可以快速判断排期优先级,研发可以直达详情页开始修问题,管理层的关注点也比较好对齐。
6.2 单条漏洞详情怎么写才有效
单条漏洞的描述决定了研发能不能快速复现并修复。我写单条漏洞时会强制包含以下要素:
- 所在文件和行号(精确到函数)
- 触发条件(需要什么权限、什么参数)
- 完整调用链(入口 → 处理 → 落点)
- 危害说明(能够造成什么后果)
- 修复建议(给代码级示例,而不是一句"建议加强校验")
漏洞名称:订单导出接口存在水平越权 风险等级:高 文件位置:com/example/trade/controller/ExportController.java:42 触发条件:已登录的普通用户,构造GET /api/order/export?orderId=1001 调用链:exportOrder() → orderService.getOrderById(orderId) → orderMapper.selectById 危害说明:任意登录用户可导出不属于自己的订单数据,涉及用户姓名、手机号、 收货地址等个人敏感信息,构成较大范围的数据泄露风险。 修复建议:在ExportController中获取当前登录用户ID,并在Service层校验 orderId是否归属于当前用户,不匹配时直接抛出异常。照着这个格式写完,研发如果还说"看不懂",那就不是表达问题,而是审计人员自己对漏洞的理解还没到位。
6.3 与研发沟通的几个原则
报告写好只是第一步,推动修复才是真正的硬仗。我吃了不少沟通上的亏之后,总结出几条原则:
第一,不要只报问题,要带方案。直接给修好的代码示例和影响范围说明。研发最反感的就是"这个地方有问题,你查查吧"这种甩锅式审计。
第二,沟通先对齐术语。很多业务研发对"水平越权""污点分析"不敏感,但对"用户A能看用户B的数据"这种说法非常敏感。先讲人话,再上术语,对话效率会高很多。
第三,给修复排优先级建议。高危漏洞必须立即修,中危漏洞排进迭代计划,低危问题登记到技术债。审计人员不做这个分层,修复就会受业务排期挤压。
第四,复测要闭环。修复完成后一定要做回归验证,确认问题真正解决。这里有个细节:不光要测修复的方案,还要测绕过修复的方式,防止研发打了个补丁之后引入了新的问题。
7. 审计技能怎么练:一条可执行的成长路线
回到标题里的security-audit-skill。技能这东西,看十篇方法论不如动手啃一个真实项目。我给想入行或者想提升的读者一条可以照着练的路线。
7.1 起步三件事
第一件事,精读五个CVE。去漏洞库里挑五个跟你主力技术栈相关的真实漏洞案例,把漏洞详情、修复提交、利用过程完整看完,然后在本地复现一下,把"触发条件"和"修复差异"记成笔记。这五组案例过完,你的漏洞敏感度会上一个台阶。
第二件事,认领一个开源项目做模拟审计。挑一个你熟悉的、用户量不算太大的开源项目,clone下来跑一遍工具扫描,再按上面说的七步流程人工过一遍重点模块。不用向项目方提交漏洞,就当练习。这个过程能让你在没有业务压力的情况下练熟整套审计路径。
第三件事,把审计结论写成博客或内部文档。写是最好的固化方式——你在写的时候才会发现自己对调用链的理解有没有缺口,对漏洞危害的判断是否清晰。我自己早期的审计能力提升,很大一部分来自写文档。
7.2 日常积累的日常动作
技能提升不是突击出来的,而是靠日常积累。我保留了几个习惯:
- 每周花一点时间看新曝出的高危CVE,重点看漏洞根因和利用条件
- 维护一套自己的审计规则集,每发现一个新模式就往Semgrep规则里加
- 在IDE里建一个"审计检查清单",每次审计前过一遍,防止漏项
- 定期回看自己写过的审计报告,总结哪些结论被研发认可、哪些被驳回,调整表达方式
这四件事的收益在短时间内看不太出来,但只要坚持半年,你的审计速度和准确度都会明显提升。
7.3 踩坑记录:这些错我犯过,你注意避开
最后分享一下我踩过的几个典型坑,算是给大家当个路标。
第一个坑:过度依赖工具输出。有一次我拿工具扫描结果直接写报告,结果被研发当场打脸——三个"高危"全是测试代码里的死代码,根本不可能被外部触达。从那以后,工具结果一律回代码核实,这条写进了我自己的工作规范。
第二个坑:只审代码不看第三方依赖。早期审一个Spring Boot项目,花了两天看业务代码,漏了依赖里的一个已知RCE漏洞。现在我的审计流程里,依赖扫描排在代码阅读之前,想跳都跳不过去。
第三个坑:忽略了事件监听和异步调用链。有一个越权漏洞藏在线程池异步回调里——接口入口做了鉴权,但异步线程里执行数据查询时拿的是缓存里的用户ID,一个批次任务涉及多个用户数据,没人校验这批数据是否都属于当前用户。审计时必须把消息队列消费者和异步线程池也纳入入口清单,不然攻击面就漏了一大块。
第四个坑:报告里只写问题不写价值。有一次汇报审计结果,领导问"这个问题到底对我们业务有多大影响",我当时答不上来。现在的习惯是每条高危漏洞后面都附一个"业务影响"描述,量化数据量、资金风险或者合规影响,这样管理层才能对安全问题有真实体感。
回到开头说的那个越权导出漏洞。那次任务最后的处理结果,不只是修掉了一个接口,而是在整个导出功能模块做了一次全面的资源属主校验清查,顺手把几个同类问题一起修掉了。这就是安全审计真正的价值——它不只是找漏洞,而是通过一次系统的"代码盘查",把某个业务模块的风险水位整体降下来。技能修炼到了一定程度,你会发现安全审计的本质是一种"系统性质疑"的能力:每看到一段代码,都会不自觉地追问一句"这里真的安全吗",然后再用证据去回答这个问题。这种能力很难量化,但它会在每一次代码评审、每一次方案设计、每一次紧急排查中,反复地为你创造价值。