AI代码审计实战:用coding agent构建security-audit-skill工作流
2026/9/23 7:26:06 网站建设 项目流程

1. 从一次代码审计的翻车现场说起

去年秋天,我接手了一个内部工具链的代码审计任务。项目不大,大概两万多行 Python 代码,涉及配置解析、文件操作、网络请求和几个自定义的加解密模块。当时我信心满满,觉得这种体量的代码,人工过一遍加上几款静态扫描工具,两三天就能收工。结果第一轮扫下来,工具报了四百多条告警,其中真正有安全价值的不到二十条。剩下的全是误报、重复告警和上下文缺失导致的假阳性。更麻烦的是,有些真正危险的模式——比如用户输入直接拼接进文件路径、异常处理里吞掉了权限校验失败——工具压根没报出来。

那次经历让我意识到一个问题:通用静态扫描工具解决的是“已知模式匹配”,而代码审计真正需要的是“结合业务上下文的推理判断”。这两者之间的鸿沟,不是靠调几个规则阈值就能填平的。后来我开始尝试把大语言模型引入审计流程,用 coding agent 的方式来做辅助分析,逐渐摸索出一套相对稳定的工作流。这套流程我给它起了个名字,叫security-audit-skill——不是什么正式框架,就是一套让 AI 编程助手在代码审计场景下真正能干活的方法论和提示词工程实践。

这篇文章适合两类人看:一类是日常需要做代码安全审查的工程师,另一类是想把 AI 编程助手用到安全场景但不知道怎么下手的开发者。我会把整个流程拆开讲清楚,包括为什么这样设计、每一步的具体操作、我踩过的坑,以及怎么判断 AI 给出的审计结论靠不靠谱。全文没有高深的理论,都是能直接上手抄作业的东西。

2. 为什么通用扫描工具在真实项目里总是不够用

2.1 静态分析工具的“模式匹配”天花板

静态应用安全测试工具的工作原理,本质上是在抽象语法树或者中间表示上跑规则。规则写的是“如果出现 A 模式,就报 B 漏洞”。比如检测 SQL 注入,规则可能是“用户输入变量直接拼接进 SQL 字符串”。这个思路在教科书级别的例子上很有效,但真实项目里的代码往往长这样:

def build_query(table_name, filters): base = f"SELECT * FROM {table_name}" if filters: conditions = " AND ".join( f"{k} = '{v}'" for k, v in filters.items() ) base += f" WHERE {conditions}" return base

这段代码里,table_namefilters都可能来自外部输入,但静态工具很难判断它们是否经过了上游的校验或白名单过滤。它只能看到字符串拼接,于是报一条 SQL 注入告警。如果上游确实做了严格的枚举校验,这就是误报;如果没做,这就是真漏洞。工具不知道,因为它看不到调用链的上下文。

更棘手的是那些“合法但危险”的模式。比如下面这段:

def read_user_file(user_id, filename): base_dir = "/data/users" path = os.path.join(base_dir, str(user_id), filename) with open(path, "r") as f: return f.read()

如果filename包含../,就可能穿越到其他用户的目录。静态工具可能会报路径穿越,也可能不报,取决于规则库的覆盖程度。但真正的问题是:这个函数在业务上是否允许filename包含路径分隔符?如果上游已经做了os.path.basename处理,这里就是安全的;如果没有,就是漏洞。工具无法回答这个问题,因为它不理解业务约束。

2.2 代码审计真正需要的是“上下文推理”

我在实际审计中发现,一个漏洞的成立通常需要三个条件同时满足:可控的输入源、危险的传播路径、缺失的防护措施。静态工具擅长检测第二个条件,但对第一个和第三个条件的判断往往很粗糙。

举个例子,一个 Web 应用里有个接口接收用户上传的压缩包,解压后读取里面的文件。静态工具会报“Zip Slip”路径穿越漏洞。但如果你去看代码,发现解压前已经校验了压缩包内所有文件的路径,确保它们都在目标目录下,那这个告警就是误报。反过来,如果校验逻辑写错了——比如只检查了文件名不包含..,但没考虑符号链接——工具可能根本不会报,因为它不理解符号链接在解压场景下的特殊风险。

这就是为什么我后来转向用 coding agent 来做辅助审计。大语言模型在理解代码意图、追踪数据流、结合业务上下文做推理方面,有天然的优势。它可以把一个函数放在整个调用链里看,可以理解变量命名的语义,可以推断开发者的意图。当然,它也会犯错,会幻觉,会漏报,所以需要一套工程化的方法来约束和验证它的输出。

2.3 coding agent 在审计场景下的独特价值

把 coding agent 引入代码审计,最大的价值不是“自动化”,而是“对话式推理”。你可以把它当成一个随时在线的安全工程师搭档,你问它“这个函数有没有问题”,它会给你分析;你质疑它的结论,它会重新审视;你给它补充业务背景,它会调整判断。

我常用的交互模式是这样的:先让 agent 通读一个模块,生成一份初步的风险清单;然后我针对每一条风险,追问它的判断依据;如果我觉得某条判断有问题,就把相关的调用链和业务约束贴给它,让它重新评估。这个过程有点像代码 review,但 agent 不会累,不会因为看了几千行代码就注意力下降,而且它可以同时记住多个模块之间的关联。

当然,这套方法也有明显的局限。agent 的上下文窗口有限,不可能一次性吞下整个大型项目;它的判断受训练数据影响,对某些冷门框架或自研协议的理解可能不到位;它还可能被代码里的注释误导——如果注释写的是“这里已经做了安全校验”,但实际代码没有,agent 可能会轻信注释。这些坑我在后面会详细讲怎么规避。

3. 搭建 security-audit-skill 工作流的四个核心组件

3.1 项目上下文注入:让 agent 先“读懂”项目

直接让 agent 看代码,它只能看到孤立的文件。要让它做出靠谱的判断,必须先给它注入项目级的上下文。我通常会在审计开始前,准备一份“项目背景包”,内容包括:

  • 技术栈说明:语言、框架、主要依赖库及其版本。比如“Python 3.10 + Flask 2.3 + SQLAlchemy 2.0”,这能帮助 agent 判断某些模式是否危险。比如在 SQLAlchemy 里用text()拼接原生 SQL 是危险的,但用 ORM 的查询构造器通常是安全的。
  • 架构概览:哪些模块处理外部输入,哪些模块做核心业务逻辑,哪些模块负责持久化。我一般会画一个简单的数据流图,用文字描述清楚“用户请求从哪进来,经过哪些处理,最后到哪去”。
  • 信任边界:明确哪些数据是可信的,哪些是不可信的。比如“来自内部配置中心的数据视为可信,来自 HTTP 请求体的数据视为不可信”。
  • 已知的安全机制:项目里已经有哪些防护措施,比如统一的输入校验中间件、参数化查询封装、权限校验注解等。这些信息能大幅减少 agent 的误报。

这份背景包不需要很正式,几百字到一千字就够了。我通常直接写在对话的开头,或者存成一个AUDIT_CONTEXT.md文件,每次审计时让 agent 先读这个文件。

提示:背景包里的信息必须准确。如果你告诉 agent “所有输入都经过了校验”,但实际有遗漏,agent 会基于错误前提做出错误判断。宁可写得保守一点,也不要过度承诺。

3.2 分块审计策略:怎么切代码才合理

大型项目不可能一次性塞给 agent。我的做法是按“功能模块 + 数据流”来切块,而不是按文件目录切。比如一个电商系统,我会切成“用户认证模块”“商品查询模块”“订单处理模块”“支付回调模块”等。每个模块内部,再按“入口层 → 业务层 → 数据层”的顺序组织代码给 agent 看。

切块的时候有几个原则:

  • 每个块不超过 2000 行代码。超过这个量,agent 的注意力会分散,漏报率明显上升。
  • 保持调用链完整。如果一个函数在 A 文件定义,在 B 文件调用,在 C 文件做校验,那这三个文件的相关部分要放在同一个块里。
  • 标注跨块依赖。如果当前块依赖另一个块的某个函数,把这个函数的签名和关键逻辑摘录过来,让 agent 知道“这个输入在上游已经被处理过了”。

我一般会写一个简单的脚本,用正则或 AST 解析来提取函数定义和调用关系,辅助切块。但最终的分块决策还是人工来做,因为只有人知道业务上的模块边界在哪里。

3.3 审计提示词的设计:问对问题比什么都重要

提示词的质量直接决定审计结果的质量。我试过很多版本,最后稳定下来的提示词结构是这样的:

你是一名资深应用安全工程师,正在对以下代码进行安全审计。 项目背景: [粘贴项目背景包] 当前模块: [模块名称和功能描述] 信任边界: [明确哪些输入可信,哪些不可信] 请按以下步骤分析: 1. 列出当前模块的所有外部输入源(函数参数、全局变量、配置读取、网络请求等)。 2. 对每个输入源,追踪它的传播路径,直到它被使用在危险操作中(文件操作、数据库查询、命令执行、反序列化、模板渲染等)。 3. 对每条传播路径,判断是否存在有效的防护措施(参数化查询、白名单校验、路径规范化、权限检查等)。 4. 只报告你确信存在安全风险的路径,并给出具体的代码行号和利用条件。 5. 对于你不确定的情况,单独列出来并说明需要什么额外信息才能判断。 输出格式: - 风险清单(按严重程度排序) - 每条风险的详细分析(输入源、传播路径、缺失的防护、利用条件) - 不确定项列表

这个提示词的关键在于:强制 agent 先追踪数据流,再下结论。很多误报是因为 agent 看到危险函数就报警,没有追溯输入是否可控。通过要求它显式列出输入源和传播路径,可以大幅降低这类误报。

另外,我特意加了“不确定项列表”这一条。agent 在遇到模糊情况时,与其强行给一个结论,不如老实说“我不确定”。这些不确定项往往是最值得人工深入看的地方。

3.4 结果验证闭环:怎么判断 agent 说得对不对

Agent 给出的每一条风险,我都会用一套固定的流程来验证:

  1. 复现输入源:找到 agent 说的输入源,确认它是否真的可控。有时候 agent 会把内部函数的参数当成外部输入,实际上这个参数只由内部代码传入,且值来自常量。
  2. 检查防护措施:在调用链的每一层查找是否有校验、过滤、转义。特别注意那些“看起来像校验但实际无效”的代码,比如用黑名单过滤../但没考虑 URL 编码。
  3. 构造 PoC:对于确认的风险,尝试构造一个最小的利用载荷。如果构造不出来,要么是风险不成立,要么是还有隐藏的防护没被发现。
  4. 交叉验证:用另一款静态工具或另一个 agent 实例重新分析同一段代码,看结论是否一致。不一致的地方重点排查。

这套流程走下来,误报率能压到很低。代价是时间,但比起漏掉一个真实漏洞的后果,这个时间花得值。

4. 实战拆解:一次完整的模块审计过程

4.1 目标模块与背景准备

我拿一个真实的内部项目来演示。这是一个文件管理服务,提供上传、下载、删除、列表四个接口。技术栈是 Python 3.10 + FastAPI + SQLite。用户通过 JWT 认证,每个用户有自己的目录。我准备的项目背景包如下:

项目:内部文件管理服务 技术栈:Python 3.10, FastAPI 0.100, SQLite 3 认证:JWT,用户 ID 从 token 中解析 信任边界: - JWT token 中的 user_id 视为可信(由认证中间件校验签名) - HTTP 请求中的路径参数、查询参数、请求体视为不可信 - 数据库中的数据视为可信(仅由本服务写入) 已知防护: - 所有接口都有认证依赖注入 - 数据库操作使用 SQLAlchemy ORM,无原生 SQL 拼接 - 文件操作使用 pathlib,但未统一做路径规范化

这个背景包很简短,但把关键信息都覆盖了。特别是最后一条“未统一做路径规范化”,直接提示 agent 重点关注路径穿越问题。

4.2 第一轮扫描:agent 发现了什么

我把文件操作相关的三个文件(routes.pyservices.pystorage.py)一起发给 agent,附上上面的提示词。Agent 返回了一份风险清单,我摘录其中三条:

风险一:下载接口的路径穿越

# routes.py @router.get("/download/{filename}") async def download_file(filename: str, user_id: str = Depends(get_current_user)): return services.get_file(user_id, filename) # services.py def get_file(user_id: str, filename: str): path = storage.get_user_path(user_id) / filename if not path.exists(): raise HTTPException(404) return FileResponse(path)

Agent 的分析是:filename来自路径参数,完全可控。storage.get_user_path(user_id)返回用户目录,然后直接拼接filename。如果filename../../etc/passwd,就能读取任意文件。没有看到任何路径规范化或白名单校验。

风险二:上传接口的文件名处理

# routes.py @router.post("/upload") async def upload_file(file: UploadFile, user_id: str = Depends(get_current_user)): content = await file.read() services.save_file(user_id, file.filename, content) # services.py def save_file(user_id: str, filename: str, content: bytes): path = storage.get_user_path(user_id) / filename path.write_bytes(content)

Agent 指出:file.filename来自客户端,虽然 FastAPI 的UploadFile会做一些处理,但文件名本身仍然可能包含路径分隔符。如果客户端发送的 filename 是../../../tmp/evil.sh,就可能写到用户目录之外。

风险三:列表接口的符号链接问题

# services.py def list_files(user_id: str): user_dir = storage.get_user_path(user_id) return [f.name for f in user_dir.iterdir() if f.is_file()]

Agent 认为这条风险较低,但提了一个点:如果用户目录里存在指向外部的符号链接,iterdir()会遍历到链接目标,is_file()也会返回 True,导致列表接口泄露外部文件的存在性。不过由于这个服务不允许用户创建符号链接(上传的是普通文件),实际风险有限。

4.3 人工复核与误报剔除

我逐条复核了 agent 的发现。

风险一和风险二确认成立。我实际构造了 PoC:用curl发送GET /download/..%2F..%2Fetc%2Fpasswd,成功读取到了系统文件。上传接口也类似,可以写到任意路径。这两个是真实的高危漏洞。

风险三我判断为低风险,因为服务本身不提供创建符号链接的功能,用户无法通过正常接口在目录里放置符号链接。但如果服务器上其他进程有权限往用户目录写文件,这个风险就会升级。我把它标记为“环境依赖型风险”,在报告里单独说明。

Agent 还漏了一个点:删除接口没有检查文件是否存在,直接调用unlink(),如果文件不存在会抛异常,虽然不构成安全漏洞,但会导致错误信息泄露路径信息。这个我在人工复核时补上了。

4.4 修复方案与二次验证

针对确认的漏洞,我写了修复代码:

# storage.py from pathlib import Path def get_safe_path(user_id: str, filename: str) -> Path: base = get_user_path(user_id).resolve() # 只取文件名部分,丢弃任何路径分隔符 safe_name = Path(filename).name target = (base / safe_name).resolve() # 双重检查:确保解析后的路径仍在用户目录下 if not str(target).startswith(str(base)): raise HTTPException(400, "Invalid filename") return target

修复的关键点有两个:一是用Path(filename).name剥离路径信息,二是用resolve()解析后做前缀检查,防止符号链接绕过。修复后我把代码重新发给 agent,让它验证是否还有绕过方式。Agent 确认了修复的有效性,但提醒我注意resolve()在文件不存在时的行为——如果目标文件不存在,resolve()仍然会返回规范化后的路径,前缀检查依然有效。这个提醒很有价值,因为我原本担心文件不存在时检查会失效。

5. 那些让我踩坑的细节和反直觉经验

5.1 Agent 会被注释和命名误导

有一次审计一个权限校验模块,代码里有个函数叫check_admin_permission,注释写着“校验当前用户是否为管理员”。Agent 看到这个函数名和注释,直接判定“权限校验已实现”。但我实际看代码发现,这个函数只是检查了用户角色字段是否等于"admin",而角色字段是从 JWT token 里直接取的,token 的签名校验中间件在另一个文件里,而且那个中间件有个配置项可以关闭签名校验——生产环境恰好没关,但测试环境关了。如果只看这个函数,会误以为权限控制很完善。

这个坑的教训是:不要让 agent 依赖命名和注释做判断,要让它看实际逻辑。我在提示词里加了一条:“忽略所有注释和函数命名,只根据实际代码逻辑判断。”这条改动之后,agent 的误判率明显下降。

5.2 上下文窗口的“中间遗忘”现象

Agent 的上下文窗口虽然大,但存在“中间遗忘”现象:放在对话开头和结尾的信息,它记得比较牢;放在中间的大段代码,它容易忽略细节。我试过把 3000 行代码一次性发给 agent,结果它只分析了前 500 行和后 500 行,中间的部分基本没看。

解决办法是分块,而且每块不要太大。我现在的做法是每块控制在 800 到 1200 行,并且在每块的开头和结尾都重复一遍关键背景信息。比如在代码块前面写“以下代码来自文件上传模块,输入源是 HTTP 请求体中的 file 参数”,在代码块后面再写一遍“再次确认:file 参数不可信,需要检查路径穿越和文件类型校验”。这种重复看起来啰嗦,但实测能显著提升 agent 的注意力。

5.3 不同语言和框架的“危险模式”差异很大

Agent 对主流语言和框架的理解比较好,但对一些冷门组合容易出错。比如我审计过一个用 Go 的text/template做 HTML 渲染的项目,agent 没有意识到text/template不会自动转义 HTML,而html/template会。它把两者混为一谈,导致漏报了一个 XSS。

后来我在提示词里加了一段“框架特定注意事项”,针对当前项目的技术栈,手动列出常见的危险 API 和安全 API。比如:

Go 模板注意事项: - text/template:不自动转义,输出到 HTML 时需手动转义 - html/template:自动转义,但使用 template.HTML 类型会绕过转义 - 危险函数:template.HTML(), template.JS(), template.URL()

这段内容是我根据经验手写的,不是让 agent 自己生成。虽然麻烦,但能有效弥补 agent 在冷门知识上的不足。

5.4 审计报告的可读性比技术深度更重要

一开始我让 agent 输出详细的技术分析,结果报告几十页,开发团队根本看不完。后来我改成“分层输出”:第一层是给开发看的修复清单,每条风险一句话描述加修复建议;第二层是给安全团队看的详细分析,包含数据流追踪和 PoC;第三层是给管理层看的风险概览,按严重程度统计。

这个分层结构是我从实际协作中总结出来的。开发人员最关心“哪里有问题、怎么改”,安全人员关心“为什么有问题、怎么验证”,管理层关心“有多少问题、优先级怎么排”。一份报告同时满足三类人,比写三份报告效率高得多。

6. 把 security-audit-skill 变成可复用的能力

6.1 沉淀自己的审计提示词库

经过多个项目的积累,我现在有一套按场景分类的提示词模板。比如“Web 接口审计”“文件操作审计”“认证授权审计”“序列化审计”等。每个模板里预置了该场景常见的危险模式、检查清单和输出格式。用的时候只需要把项目背景和代码贴进去,微调一下就能用。

这套模板库的价值在于:它把我个人的审计经验固化了。即使换一个完全不熟悉的项目,只要场景匹配,提示词就能引导 agent 关注正确的方向。我建议每个做代码审计的人,都花时间整理自己的提示词库,哪怕一开始只有三五个模板,用起来之后会越来越顺手。

6.2 和 CI 流程的结合方式

我把 security-audit-skill 接入了 CI,但不是让它做全量审计——那样太慢,而且误报会阻塞构建。我的做法是:在 PR 阶段,只对变更的文件做增量审计。Agent 分析 diff 里的新增代码,如果发现高危模式,就在 PR 里留评论。这样既不会拖慢开发节奏,又能拦住大部分“新引入的漏洞”。

增量审计的提示词和全量审计不同,重点是“只看新增代码,但要考虑它和现有代码的交互”。比如新增了一个函数,要检查它的输入是否来自已有的不可信源,它的输出是否流入了已有的危险操作。这种跨变更的上下文,需要在提示词里显式提供。

6.3 人工审计师的角色变化

用了这套方法之后,我的角色从“逐行看代码”变成了“设计审计策略、验证 agent 结论、处理模糊情况”。工作量没有减少,但产出质量提高了。以前一天只能仔细看两三千行代码,现在一天能覆盖上万行,而且漏报率更低。

但我也清楚,agent 不能替代人的判断。它擅长的是模式识别和数据流追踪,不擅长的是理解业务意图和权衡风险。比如一个接口返回了详细的错误信息,从安全角度这是信息泄露,但如果这个接口是内部调试用的,且只对管理员开放,那风险就可以接受。这种判断需要人来做。

6.4 持续更新的必要性

安全审计不是一次性的工作。新的漏洞模式、新的框架特性、新的攻击手法不断出现,agent 的知识库和我的提示词库都需要持续更新。我现在的做法是:每完成一个项目,把新发现的误报模式和漏报模式记录下来,更新到提示词里;每季度回顾一次,把过时的内容清理掉。

这个习惯看起来不起眼,但坚持下来,审计效率的提升是复利式的。第一年可能只是省了一些重复劳动,第三年你会发现,新项目的审计速度比老项目快得多,因为大部分常见问题已经被提示词覆盖了,agent 一上来就能抓住重点。

最后分享一个我最近在用的技巧:让 agent 在审计结束后,自己生成一份“本次审计的局限性说明”,列出它不确定的地方、可能遗漏的场景、需要人工确认的点。这份说明往往比审计报告本身更有价值,因为它诚实地告诉你“哪里可能有问题但没查出来”。我每次都会认真看这份说明,然后针对性地做补充审计。这个习惯帮我发现了好几个 agent 漏掉但确实存在的漏洞。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询