1
月
在社区成长 1 月
LEVEL 1
1 XP
一、背景:为什么代码审计不能只靠大模型
在安全研发和安全运营中,大模型适合解释代码、归纳数据流、生成修复建议,但不适合在缺少证据时直接判定漏洞。实际项目常见两个极端:一是把整个仓库直接交给模型,结果受到上下文长度、依赖缺失和提示注入影响;二是仅让模型解释静态扫描告警,输出看似专业,却没有验证变量是否真正可控、危险函数是否实际可达。
更可靠的方案是采用“静态分析负责发现候选点,RAG 负责补充项目上下文,大模型负责推理与说明,规则和人工负责最终门控”的组合。本文给出一套可在本地仓库或授权靶场中实施的流程,重点不是让模型替代审计人员,而是缩短告警分诊、调用链阅读和修复验证所需的时间。
二、整体原理
流水线可拆成五层:
1. 候选发现层:使用 Semgrep、CodeQL、编译器告警或依赖扫描器发现疑似问题。
2. 上下文检索层:按函数、类、路由和调用关系检索相关代码,而不是盲目切分固定长度文本。
3. 证据推理层:要求模型分别列出输入源 Source、传播路径 Path、危险汇点 Sink、净化措施 Sanitizer 和必要前提 Preconditions。
4. 验证门控层:通过单元测试、污点分析、人工复核或本地动态测试确认漏洞是否成立。
5. 评测与反馈层:记录误报、漏报、证据引用和修复结果,形成可回归的数据集。
核心原则是“结论必须绑定证据”。若模型不能指出具体文件、行号、调用路径及触发条件,则结果只能标记为待确认,不能直接升级为漏洞工单。
三、前置条件
建议准备以下环境:
1. 一个仅用于测试的本地代码仓库,或 OWASP Juice Shop、WebGoat 等明确授权的靶场。
2. Python 3.10 以上环境,以及 Docker 或项目自身的隔离运行方式。
3. Semgrep 社区版;复杂跨文件分析可选用 CodeQL。
4. 一个可本地部署或通过合规接口调用的大模型。若源码敏感,应优先选择内网推理服务,并关闭请求日志中的源码留存。
5. 向量数据库不是必需品。小型仓库可先用 ripgrep、符号索引和 SQLite;只有在仓库规模扩大后,再引入向量检索。
安装基础工具:
python -m venv .venv
source .venv/bin/activate
pip install semgrep tree-sitter rank-bm25
首次实践不要直接扫描全部历史代码。建议选择一个服务、一个漏洞类型和一批已人工确认的样本,例如先处理 Python Web 项目中的命令注入或 SQL 注入。
四、分步骤实践
步骤 1:建立可复现的扫描基线
在本地仓库根目录执行:
semgrep scan --config auto --json --output semgrep.json .
不要只保存终端文本,应保留 JSON 中的规则 ID、文件路径、起止位置和元变量。为了让后续评测可复现,还应记录仓库提交号:
git rev-parse HEAD > scan_commit.txt
生产环境中应固定 Semgrep 版本和规则版本,否则同一提交的结果可能随规则更新变化。可以在 CI 中保存工具版本:
semgrep --version > semgrep_version.txt
步骤 2:对候选点进行结构化扩展
假设扫描器发现某个 subprocess 调用。仅截取告警所在的一行通常不够,应补充:
1. 当前函数的完整语法节点。
2. 调用当前函数的上游函数或路由。
3. 被调用包装函数的实现。
4. 相关配置、输入校验器和测试代码。
5. 告警前后少量代码,便于保留变量赋值关系。
检索顺序建议采用“符号精确检索优先,关键词与向量检索补充”。例如先用 ripgrep 查找函数引用:
rg -n 'run_report|subprocess\.run|shell=True' src tests
对 Python、JavaScript、Java 等语言,可借助 Tree-sitter 按函数或类切块。每个代码块应附带元数据:仓库提交号、文件路径、起止行、符号名、语言和检索原因。不要只把代码正文写入向量库,否则模型给出的引用难以追踪。
步骤 3:设计强约束审计提示
建议要求模型输出固定字段,而不是自由发挥。输入至少包括扫描规则说明、候选代码、相关调用方和项目约束。任务约束可以写成:
- 仅依据所提供的代码和证据分析,不补造不存在的路由、参数或框架行为。
- 明确列出 Source、Path、Sink、Sanitizer、Preconditions。
- 每条证据必须包含文件路径和行号范围。
- 若输入是否外部可控、路径是否可达或净化是否有效无法确定,结论必须为 NEEDS_REVIEW。
- 风险等级与置信度分开给出。高危但证据不足,不等于高置信度。
- 只给出防御性修复,不生成面向真实系统的利用步骤。
推荐的结构化输出字段包括:finding_id、verdict、weakness、evidence、dataflow、preconditions、severity、confidence、fix、verification_test。其中 verdict 只能取 CONFIRMED、LIKELY、FALSE_POSITIVE、NEEDS_REVIEW 四种值。
步骤 4:用本地示例验证推理链
下面是一个仅用于本地实验的 Python 示例:
from flask import Flask, request
import subprocess
app = Flask(__name__)
@app.get('/report')
def report():
name = request.args.get('name', '')
subprocess.run('generate-report ' + name, shell=True, check=True)
return 'ok'
可靠分析不应只看到 shell=True 就结束,而应形成完整证据链:name 来自 HTTP 查询参数,是外部输入;字符串拼接后进入 subprocess.run;shell=True 启用 shell 解析;代码中未见白名单或参数化处理。因此,在该局部上下文成立的前提下,可以将其判定为高风险候选。若路由仅在测试环境启用、上游网关存在不可绕过的严格约束,风险判断还需结合部署事实。
防御性修复应避免拼接命令,并关闭 shell:
@app.get('/report')
def report():
name = request.args.get('name', '')
if not name.isalnum():
return 'invalid', 400
subprocess.run(['generate-report', name], shell=False, check=True)
return 'ok'
这里的重点是参数数组和 shell=False。字符校验属于额外的业务约束,不能代替安全调用方式。修复后可编写单元测试,确认非法输入被拒绝、合法输入仍可工作,并使用 mock 检查 subprocess.run 收到的是参数列表。
步骤 5:加入证据门控
在自动创建安全工单前设置硬性条件。例如:
1. 至少存在一个可定位的 Source 和 Sink。
2. 数据流路径上的每一步都有代码引用。
3. 未发现明确有效的 Sanitizer,或已有证据说明净化不适用于当前上下文。
4. 置信度达到预设阈值。
5. 高风险结果必须经过人工复核或测试确认。
可以采用简单评分:静态规则命中加 2 分;Source 和 Sink 均可定位加 2 分;跨函数路径完整加 2 分;存在对应测试加 2 分;只有模型主观推断则不加分。评分并非漏洞严重度,而是证据完整度。两者必须分开存储。
步骤 6:进行修复验证
修复完成后至少执行三类检查:
semgrep scan --config auto .
pytest -q
python -m compileall src
此外,应比较修复前后的调用方式,防止开发者仅通过注释、重命名或规则忽略标记消除告警。对于 SQL 注入,应验证是否改为参数化查询;对于路径穿越,应验证规范化后的路径仍位于允许根目录;对于反序列化风险,应确认是否更换为安全格式或启用可信类型白名单。
五、如何评测系统是否真的有用
不要只统计模型生成了多少报告。建议建立人工标注的小型金标准集,至少包含真实阳性、明显误报、证据不足和已正确修复四类样本,并跟踪:
1. Precision:模型标记为确认的问题中,真正成立的比例。
2. Recall:已知问题中被系统发现并正确分类的比例。
3. Evidence Accuracy:引用的文件和行号是否真正支持结论。
4. Abstention Quality:证据不足时,模型是否愿意输出 NEEDS_REVIEW。
5. Fix Validity:修复建议是否消除根因,同时保持测试通过。
6. Review Time:审计人员处理单个告警的中位耗时是否下降。
安全场景中,适度拒答比自信编造更可靠。评测集也要防止数据泄漏:如果示例、答案和提示模板完全相同,结果无法代表对新代码的泛化能力。可以按仓库或时间切分训练示例与测试样本。
六、风险与注意事项
第一,源码泄露风险。调用外部模型前,应确认数据处理条款,并对密钥、令牌、客户信息和内部域名进行脱敏。最好在检索前执行 Secret Scanner,而不是依赖提示词要求模型忽略秘密。
第二,仓库提示注入。攻击者可能在注释、README 或字符串中写入“忽略审计规则”等内容。代码仓库中的自然语言只能视为待分析数据,不能成为系统指令。系统提示、工具权限和检索内容必须分层隔离。
第三,上下文截断。若关键净化函数未被检索,模型会误报;若关键调用方未被检索,模型会漏掉外部可控性。因此需要在报告中展示“已检索范围”,让审计人员知道结论覆盖了什么。
第四,自动修复风险。禁止让模型在无审查情况下直接合并安全补丁。自动生成的补丁可能破坏鉴权、错误处理和兼容性,也可能只绕过扫描规则。更稳妥的方式是生成独立分支、附带测试并由代码所有者审批。
第五,动态验证边界。所有测试必须在本地、隔离环境或明确授权的靶场中进行。不要把模型生成的探测请求发送给未知公网目标,也不要让审计代理默认拥有生产凭据和任意网络访问权限。
七、常见问题
问:向量检索是否一定优于关键词检索?
答:不一定。函数名、类名、路由和危险 API 更适合精确检索。向量检索适合寻找语义相关的封装函数或设计文档。实践中通常采用符号索引、BM25 和向量结果融合,再按文件关系重排。
问:为什么扫描器已经有告警,还需要大模型?
答:扫描器擅长稳定发现模式,大模型更适合把跨文件上下文整理为可读的数据流、解释误报原因并提出与项目风格一致的修复方案。二者是互补关系。
问:能否让模型直接给出漏洞等级?
答:可以作为建议,但不能脱离资产暴露面、权限边界和部署配置。代码层严重度、利用前提、业务影响和证据置信度应分别记录,再由组织现有的风险模型汇总。
问:如何降低误报?
答:优先补齐调用图、配置和净化函数;针对常见框架编写项目规则;要求模型给出反证;将已确认误报保存为回归样本,但不要简单把某个函数永久加入忽略列表。
八、总结
大模型在代码审计中的可靠价值,不是凭空发现“神秘漏洞”,而是把扫描结果、项目上下文、数据流证据和修复验证组织成可复核的过程。一个可上线的方案至少应具备结构化检索、证据引用、保守拒答、人工门控、隔离测试和持续评测。建议从单一语言、单一漏洞类型和几十个已标注告警开始,先验证误报率与审计耗时,再逐步扩展到更多仓库。只有当每个结论都能回到具体代码、明确前提和可重复测试时,这套系统才真正适合进入安全运营流水线。
在安全研发和安全运营中,大模型适合解释代码、归纳数据流、生成修复建议,但不适合在缺少证据时直接判定漏洞。实际项目常见两个极端:一是把整个仓库直接交给模型,结果受到上下文长度、依赖缺失和提示注入影响;二是仅让模型解释静态扫描告警,输出看似专业,却没有验证变量是否真正可控、危险函数是否实际可达。
更可靠的方案是采用“静态分析负责发现候选点,RAG 负责补充项目上下文,大模型负责推理与说明,规则和人工负责最终门控”的组合。本文给出一套可在本地仓库或授权靶场中实施的流程,重点不是让模型替代审计人员,而是缩短告警分诊、调用链阅读和修复验证所需的时间。
二、整体原理
流水线可拆成五层:
1. 候选发现层:使用 Semgrep、CodeQL、编译器告警或依赖扫描器发现疑似问题。
2. 上下文检索层:按函数、类、路由和调用关系检索相关代码,而不是盲目切分固定长度文本。
3. 证据推理层:要求模型分别列出输入源 Source、传播路径 Path、危险汇点 Sink、净化措施 Sanitizer 和必要前提 Preconditions。
4. 验证门控层:通过单元测试、污点分析、人工复核或本地动态测试确认漏洞是否成立。
5. 评测与反馈层:记录误报、漏报、证据引用和修复结果,形成可回归的数据集。
核心原则是“结论必须绑定证据”。若模型不能指出具体文件、行号、调用路径及触发条件,则结果只能标记为待确认,不能直接升级为漏洞工单。
三、前置条件
建议准备以下环境:
1. 一个仅用于测试的本地代码仓库,或 OWASP Juice Shop、WebGoat 等明确授权的靶场。
2. Python 3.10 以上环境,以及 Docker 或项目自身的隔离运行方式。
3. Semgrep 社区版;复杂跨文件分析可选用 CodeQL。
4. 一个可本地部署或通过合规接口调用的大模型。若源码敏感,应优先选择内网推理服务,并关闭请求日志中的源码留存。
5. 向量数据库不是必需品。小型仓库可先用 ripgrep、符号索引和 SQLite;只有在仓库规模扩大后,再引入向量检索。
安装基础工具:
python -m venv .venv
source .venv/bin/activate
pip install semgrep tree-sitter rank-bm25
首次实践不要直接扫描全部历史代码。建议选择一个服务、一个漏洞类型和一批已人工确认的样本,例如先处理 Python Web 项目中的命令注入或 SQL 注入。
四、分步骤实践
步骤 1:建立可复现的扫描基线
在本地仓库根目录执行:
semgrep scan --config auto --json --output semgrep.json .
不要只保存终端文本,应保留 JSON 中的规则 ID、文件路径、起止位置和元变量。为了让后续评测可复现,还应记录仓库提交号:
git rev-parse HEAD > scan_commit.txt
生产环境中应固定 Semgrep 版本和规则版本,否则同一提交的结果可能随规则更新变化。可以在 CI 中保存工具版本:
semgrep --version > semgrep_version.txt
步骤 2:对候选点进行结构化扩展
假设扫描器发现某个 subprocess 调用。仅截取告警所在的一行通常不够,应补充:
1. 当前函数的完整语法节点。
2. 调用当前函数的上游函数或路由。
3. 被调用包装函数的实现。
4. 相关配置、输入校验器和测试代码。
5. 告警前后少量代码,便于保留变量赋值关系。
检索顺序建议采用“符号精确检索优先,关键词与向量检索补充”。例如先用 ripgrep 查找函数引用:
rg -n 'run_report|subprocess\.run|shell=True' src tests
对 Python、JavaScript、Java 等语言,可借助 Tree-sitter 按函数或类切块。每个代码块应附带元数据:仓库提交号、文件路径、起止行、符号名、语言和检索原因。不要只把代码正文写入向量库,否则模型给出的引用难以追踪。
步骤 3:设计强约束审计提示
建议要求模型输出固定字段,而不是自由发挥。输入至少包括扫描规则说明、候选代码、相关调用方和项目约束。任务约束可以写成:
- 仅依据所提供的代码和证据分析,不补造不存在的路由、参数或框架行为。
- 明确列出 Source、Path、Sink、Sanitizer、Preconditions。
- 每条证据必须包含文件路径和行号范围。
- 若输入是否外部可控、路径是否可达或净化是否有效无法确定,结论必须为 NEEDS_REVIEW。
- 风险等级与置信度分开给出。高危但证据不足,不等于高置信度。
- 只给出防御性修复,不生成面向真实系统的利用步骤。
推荐的结构化输出字段包括:finding_id、verdict、weakness、evidence、dataflow、preconditions、severity、confidence、fix、verification_test。其中 verdict 只能取 CONFIRMED、LIKELY、FALSE_POSITIVE、NEEDS_REVIEW 四种值。
步骤 4:用本地示例验证推理链
下面是一个仅用于本地实验的 Python 示例:
from flask import Flask, request
import subprocess
app = Flask(__name__)
@app.get('/report')
def report():
name = request.args.get('name', '')
subprocess.run('generate-report ' + name, shell=True, check=True)
return 'ok'
可靠分析不应只看到 shell=True 就结束,而应形成完整证据链:name 来自 HTTP 查询参数,是外部输入;字符串拼接后进入 subprocess.run;shell=True 启用 shell 解析;代码中未见白名单或参数化处理。因此,在该局部上下文成立的前提下,可以将其判定为高风险候选。若路由仅在测试环境启用、上游网关存在不可绕过的严格约束,风险判断还需结合部署事实。
防御性修复应避免拼接命令,并关闭 shell:
@app.get('/report')
def report():
name = request.args.get('name', '')
if not name.isalnum():
return 'invalid', 400
subprocess.run(['generate-report', name], shell=False, check=True)
return 'ok'
这里的重点是参数数组和 shell=False。字符校验属于额外的业务约束,不能代替安全调用方式。修复后可编写单元测试,确认非法输入被拒绝、合法输入仍可工作,并使用 mock 检查 subprocess.run 收到的是参数列表。
步骤 5:加入证据门控
在自动创建安全工单前设置硬性条件。例如:
1. 至少存在一个可定位的 Source 和 Sink。
2. 数据流路径上的每一步都有代码引用。
3. 未发现明确有效的 Sanitizer,或已有证据说明净化不适用于当前上下文。
4. 置信度达到预设阈值。
5. 高风险结果必须经过人工复核或测试确认。
可以采用简单评分:静态规则命中加 2 分;Source 和 Sink 均可定位加 2 分;跨函数路径完整加 2 分;存在对应测试加 2 分;只有模型主观推断则不加分。评分并非漏洞严重度,而是证据完整度。两者必须分开存储。
步骤 6:进行修复验证
修复完成后至少执行三类检查:
semgrep scan --config auto .
pytest -q
python -m compileall src
此外,应比较修复前后的调用方式,防止开发者仅通过注释、重命名或规则忽略标记消除告警。对于 SQL 注入,应验证是否改为参数化查询;对于路径穿越,应验证规范化后的路径仍位于允许根目录;对于反序列化风险,应确认是否更换为安全格式或启用可信类型白名单。
五、如何评测系统是否真的有用
不要只统计模型生成了多少报告。建议建立人工标注的小型金标准集,至少包含真实阳性、明显误报、证据不足和已正确修复四类样本,并跟踪:
1. Precision:模型标记为确认的问题中,真正成立的比例。
2. Recall:已知问题中被系统发现并正确分类的比例。
3. Evidence Accuracy:引用的文件和行号是否真正支持结论。
4. Abstention Quality:证据不足时,模型是否愿意输出 NEEDS_REVIEW。
5. Fix Validity:修复建议是否消除根因,同时保持测试通过。
6. Review Time:审计人员处理单个告警的中位耗时是否下降。
安全场景中,适度拒答比自信编造更可靠。评测集也要防止数据泄漏:如果示例、答案和提示模板完全相同,结果无法代表对新代码的泛化能力。可以按仓库或时间切分训练示例与测试样本。
六、风险与注意事项
第一,源码泄露风险。调用外部模型前,应确认数据处理条款,并对密钥、令牌、客户信息和内部域名进行脱敏。最好在检索前执行 Secret Scanner,而不是依赖提示词要求模型忽略秘密。
第二,仓库提示注入。攻击者可能在注释、README 或字符串中写入“忽略审计规则”等内容。代码仓库中的自然语言只能视为待分析数据,不能成为系统指令。系统提示、工具权限和检索内容必须分层隔离。
第三,上下文截断。若关键净化函数未被检索,模型会误报;若关键调用方未被检索,模型会漏掉外部可控性。因此需要在报告中展示“已检索范围”,让审计人员知道结论覆盖了什么。
第四,自动修复风险。禁止让模型在无审查情况下直接合并安全补丁。自动生成的补丁可能破坏鉴权、错误处理和兼容性,也可能只绕过扫描规则。更稳妥的方式是生成独立分支、附带测试并由代码所有者审批。
第五,动态验证边界。所有测试必须在本地、隔离环境或明确授权的靶场中进行。不要把模型生成的探测请求发送给未知公网目标,也不要让审计代理默认拥有生产凭据和任意网络访问权限。
七、常见问题
问:向量检索是否一定优于关键词检索?
答:不一定。函数名、类名、路由和危险 API 更适合精确检索。向量检索适合寻找语义相关的封装函数或设计文档。实践中通常采用符号索引、BM25 和向量结果融合,再按文件关系重排。
问:为什么扫描器已经有告警,还需要大模型?
答:扫描器擅长稳定发现模式,大模型更适合把跨文件上下文整理为可读的数据流、解释误报原因并提出与项目风格一致的修复方案。二者是互补关系。
问:能否让模型直接给出漏洞等级?
答:可以作为建议,但不能脱离资产暴露面、权限边界和部署配置。代码层严重度、利用前提、业务影响和证据置信度应分别记录,再由组织现有的风险模型汇总。
问:如何降低误报?
答:优先补齐调用图、配置和净化函数;针对常见框架编写项目规则;要求模型给出反证;将已确认误报保存为回归样本,但不要简单把某个函数永久加入忽略列表。
八、总结
大模型在代码审计中的可靠价值,不是凭空发现“神秘漏洞”,而是把扫描结果、项目上下文、数据流证据和修复验证组织成可复核的过程。一个可上线的方案至少应具备结构化检索、证据引用、保守拒答、人工门控、隔离测试和持续评测。建议从单一语言、单一漏洞类型和几十个已标注告警开始,先验证误报率与审计耗时,再逐步扩展到更多仓库。只有当每个结论都能回到具体代码、明确前提和可重复测试时,这套系统才真正适合进入安全运营流水线。