1. 为什么我决定给 AI 写的代码做一次安全体检
最近半年,我大概有三分之一的新代码是 AI 帮我写的。Copilot、ChatGPT、Cursor 轮着用,写工具脚本、写接口、写测试的时候,确实省了很多事。可每到上线节点,我总会多犹豫一步:这些 AI 生成的代码,表面上能跑、逻辑也对,但安全上真的扛得住吗?带着这个疑问,我专门对一个由 AI 生成的小项目做了一轮完整的代码安全体检,从静态分析、依赖审计、密钥排查到部署配置逐项过了一遍。我写这篇就是想把这轮体检的流程和结论摊开来讲清楚,也给所有用过 AI 编程、准备让 AI 代码上线的同学一个可以直接照做的参考。
所谓“安全体检”,不是靠人眼一行行审,而是把静态分析、依赖扫描、密钥检测、容器配置检查这些自动化手段组合起来,像体检套餐一样逐层检查。后面我会用一个 AI 写的 TODO API 项目当例子,完整跑一遍这轮流程,顺便把那些扫描器输出、修复方案、踩坑记录都放出来。你下次再拿到一段 AI 生成的代码,哪怕不是我这套工具链,也可以按同样的思路去验一验。
1.1 AI 生成代码的底层逻辑,决定了它不可能天然安全
先说明白一件事:AI 大模型写代码的原理,是根据训练语料做概率预测。训练语料里有大量的开源项目、技术博客、问答帖子和历史代码,其中既有优秀代码,也有充斥着漏洞的老代码。模型在学习“这段代码后面最可能接什么”的时候,并不会真的理解什么是鉴权、什么是注入、什么是密钥管理,它只是在模仿“看起来合理”的代码形态。
这就导致一个很实际的结果:AI 生成的代码经常功能完整、格式漂亮,但安全属性是缺失的。我见过 AI 写登录接口时直接把 JWT 密钥写死在代码里,因为训练语料里最常见的做法就是这样;也见过 AI 写数据库操作时熟练地用字符串拼接 SQL,因为网络上大量 SQL 注入漏洞样例就是这么教的。模型不会站在攻击者视角去审视自己生成的东西,它不会想“这个用户输入会不会被构造来绕过校验”,也不会想“这个密钥一旦推到公开仓库会有什么后果”。
类比一下:一个按菜谱学做菜的厨师,可能很擅长把菜摆盘摆得好看,但未必懂食品安全。食材怎么保存、生熟怎么分开、哪些食材容易导致过敏,这些知识不会天然长在厨师脑子里。AI 写代码也是一样,它擅长“把需求变成代码”,但不擅长“把代码变得安全”。这个前提想清楚,后面的体检流程才有意义:我们不能指望 AI 自觉写出安全代码,只能在它交作业之后,用一套严谨的检查手段把问题翻出来。
1.2 AI 生成代码最容易翻车的四类场景
结合我自己的实际体会,AI 生成代码在安全上最容易出问题的场景大概有四类。这不是说 AI 写的代码一定烂,而是说这几类问题是高发区,体检时要重点盯。
| 风险类型 | 典型表现 | 后果示例 |
|---|---|---|
| 认证与授权 | 硬编码密钥、校验逻辑不完整、默认口令 | 任意用户伪造身份,越权访问 |
| 注入类 | 字符串拼接 SQL、命令拼接、模板注入 | 数据库被拖取,服务器被远程执行命令 |
| 敏感信息泄漏 | 日志里打印密码、接口返回堆栈信息、密钥写进代码 | 凭据泄露,被批量扫描利用 |
| 供应链风险 | 引入不存在的依赖、使用过旧版本、版本未锁定 | 依赖漏洞被利用,恶意同名包劫持 |
认证授权这块最典型。AI 知道要加一个 token,但不一定会考虑 token 过期时间、刷新机制、角色权限边界;有时候它甚至会把“检查是否登录”和“检查是否有权限”混在一起。注入类的坑也同样常见,因为大量的示例代码为了可读性会直接拼接字符串,AI 学到的就是这种写法。敏感信息泄漏更不用说,很多 AI 生成的代码会把配置项、密钥、连接串直接放在文件顶部,开发时方便,上线后就成了一个炸药包。供应链风险属于“藏得最深”的一类,AI 可能告诉你“可以用这个库”,但那个包名可能拼错了、版本过旧,甚至根本不存在,而它提供的 import 语句却看起来很合理。
体检的目的不是从此不信任 AI,而是把上面四类问题在正式上线之前全部暴露出来。接下来我就把这轮体检的具体做法拆开讲。
2. 代码安全体检该查什么:五维检查清单
给代码做安全体检,我习惯把它分成五个维度:静态分析、依赖审计、密钥扫描、部署配置检查、人工 Code Review。五个维度各管一段,组合起来才能覆盖“代码本身、第三方组件、敏感信息、运行环境、业务逻辑”这几张安全边界。
2.1 静态应用安全测试(SAST):先从代码本身找问题
静态应用安全测试,简称 SAST,核心思路是在不运行代码的情况下,对源代码做模式匹配、数据流分析、控制流分析,找出明显的漏洞模式。你可以把它理解成给代码做一次 X 光扫描,不用把程序跑起来,就能看到骨头里有没有裂缝。
我用过且比较推荐的 SAST 工具有这几个:
- Semgrep:写规则方便,内置规则多,适合快速接入和定制团队自己的规范。
- CodeQL:GitHub 出的深度分析工具,能追数据流,比如用户输入一路进 SQL 查询这种路径,它查得很准,但学习成本偏高。
- SonarQube:偏“代码质量+安全”一体,适合搭平台,能跟踪历史变化。
- Bandit:专门扫 Python 代码的轻量工具,装完就能跑。
建议第一次跑的时候先用 Semgrep 加官方默认规则,再按语言补一个专用工具,比如 Python 项目加 Bandit,Go 项目加 gosec,Node.js 项目可以考虑用内置的 eslint 安全插件。这套组合免费、开源,也能直接跑在本地,不用把代码上传到任何第三方平台。
2.2 软件供应链与依赖扫描(SCA):漏洞往往藏在第三方包里
很多 AI 生成代码的项目,直接依赖加上传递依赖动不动就上百个,人眼根本看不过来。软件成分分析(SCA)要解决的就是这个问题:扫描你的依赖清单,和公开的 CVE 漏洞库做比对,告诉你哪个包、哪个版本有已知漏洞。
常用的工具也相对固定。OWASP Dependency-Check 是特别知名的老牌工具,支持 Java、Python、JavaScript 等主流生态;Trivy 更全能,除了扫描文件系统依赖,还能扫容器镜像;Python 项目可以用 pip-audit,Node 项目可以用 npm audit,Go 项目可以用 govulncheck。我一般会先跑语言自带的审计命令,再用 Trivy 做一次全量扫描,交叉验证结果。
这轮检查在 AI 生成的代码项目里格外重要。因为 AI 经常“即兴发挥”地给某个需求推荐一个第三方库,而这个库可能已经几年没人维护了,或者某个子依赖存在公开漏洞。代码是你自己写的,但风险可能来自那些你根本没读过一行源码的包。
2.3 密钥与敏感信息扫描:别把密钥提交进仓库
密钥扫描要单独拿出来说,是因为这类问题经常不在“当前代码”里,而在 git 历史里。你以为你删掉了一个配置文件就算清理干净了,但 commit 历史里还躺着那串密钥,任何有仓库访问权限的人都能翻出来。
gitleaks 和 trufflehog 是我用得最多的两款工具。gitleaks 可以扫当前工作区,也可以扫 git 历史;trufflehog 更偏内容挖掘,能查出很多不容易被发现的 token 格式。运行方式是扫描你本地仓库的全部历史提交,一旦发现疑似密钥、密码、云厂商访问凭证就会告警。这个环节最容易被忽视,但它往往是“上线前体检”里最救命的一步。
2.4 部署配置与容器体检
代码本身没问题,不代表部署环境没问题。AI 生成项目时经常会附带生成 Dockerfile 和 docker-compose 文件,这些文件里的安全坑一点都不少:基础镜像版本过旧,容器以 root 身份运行,依赖的中间件使用默认口令,暴露了不必要的端口,缺少健康检查。
容器和配置扫描可以用 Trivy 扫镜像,用 kube-bench 检查 Kubernetes 配置基线,如果用到 Terraform 这类基础设施即代码,也可以加 tfsec 做静态检查。值得强调的是,这类体检的价值在于提前发现“运行时风险”,而不是等镜像已经推上线、容器被扫出来打补丁之后再去补救。
2.5 人工 Code Review:工具替代不了的那 20%
自动化工具再强,也替代不了人工 Code Review,特别是业务逻辑层面的安全问题。工具很难判断“这个用户可以操作另一个用户的数据”是不是越权,也很难判断“这个回调接口是不是应该限频”。这些需要人结合业务上下文去看。
我自己的做法是:跑完自动化扫描之后,把扫描结果作为 Code Review 的一份参考资料,再对照一份人工检查清单过一遍,重点关注认证覆盖范围、权限模型、输入校验、错误处理、敏感数据输出这几个点。五个维度全部走完,才算完成一次真正意义上的代码安全体检。
3. 体检实战:给一个 AI 写的 TODO API 跑一轮
理论说完了,下面进入实战。我造了一个典型的 AI 生成项目:一个用 FastAPI 写的 TODO API,带 JWT 认证、SQLite 存储、文件上传下载功能,大约 800 行代码。项目大部分内容由 AI 生成,表面功能运行正常,接下来我对它做一次完整的安全体检。
3.1 被检对象:一个看起来很完整的 TODO API
项目文件结构大致这样:
app/ main.py # FastAPI 入口,定义路由 auth.py # 登录、JWT 签发与校验 db.py # 数据库连接与查询 files.py # 文件上传下载 Dockerfile requirements.txt从功能测试来看,接口能正常跑,登录能拿到 token,创建 TODO 能写库,上传文件能保存。这些掩盖了一个事实:安全体检不是看“功能通不通”,而是看“攻击路径顺不顺”。接着我按顺序跑工具,第一轮出来的结果就很刺激。
3.2 第一轮扫描:静态分析抓到 6 个问题
用 Semgrep 和 Bandit 分别跑了一遍,最终确认了 6 个有效问题,其中 2 个被评为高危。
第一个高危问题是 auth.py 里的硬编码 JWT 密钥。代码写着SECRET_KEY = "my-super-secret-key",训练语料里这种写法太常见了,AI 直接照搬。攻击者拿到这个密钥,可以自己伪造任意用户的 token。修复很简单,改成从环境变量读取,并保证生产环境使用足够长的随机密钥。
第二个高危问题是 db.py 里拼接 SQL。原代码大概长这样:
query = f"SELECT * FROM todos WHERE user_id = '{user_id}'" cursor.execute(query)这是教科书级别的 SQL 注入写法。AI 只保证了接口能返回数据,没考虑 user_id 可以被构造。修复时改成参数化查询:
query = "SELECT * FROM todos WHERE user_id = ?" cursor.execute(query, (user_id,))另外 4 个中低危问题也值得一提。files.py 的文件下载接口直接用用户输入拼路径,存在路径穿越风险;认证码生成用了random模块,弱随机数在安全场景里不可接受;YAML 解析用了yaml.load()没指定安全加载器;还有一处日志把用户密码原样打了出来。每个问题都对应一个很具体的修复方向,核心原则只有一条:用户输入永远不可信,运行环境里的秘密永远不要硬编码。
3.3 第二轮扫描:依赖漏洞比想象中严重
代码扫描完,我开始看依赖。项目里只有一个 requirements.txt,粗看很干净:fastapi、uvicorn、python-jose、passlib、python-multipart。但跑完pip-audit之后,结果让人有点冒汗。
扫描结果显示,项目中某个校验库的版本存在已知的高危 CVE,原因是它依赖了一个不再维护的加密库;另外 fastapi 的某个次要版本也引入了低危漏洞。这里要提醒一句:pip list和requirements.txt只能看到直接依赖,真正的风险经常藏在传递依赖里。AI 生成代码时通常会写“安装什么库”,但很少会去确认这个库的依赖树里有没有已知问题。
修复思路是升级到安全版本,然后重新扫描确认漏洞消失。如果项目用的某个老库没有新版本可选,那就要进入风险登记流程,明确谁来跟进、什么时候处理、有什么替代方案。依赖层面的审计从来没有“扫完一次就结束”的说法,因为漏洞库每天都在更新。
3.4 第三轮扫描:最惊险的一处
这轮用的是 gitleaks,扫描整个 git 历史。项目当前代码里其实已经看不到明文密钥,因为我和 AI 来回改了几版,后来把配置抽到了环境变量文件里。但 gitleaks 还是从历史 commit 里翻出了一串 AWS Access Key。
这个字段通常会让很多团队从“感觉良好”瞬间变脸。因为密钥一旦进过 git 历史,即使后面删了,也已经视为泄露。第一步是立刻去云控制台轮换密钥,撤销旧凭证;第二步才是从 git 历史中清理掉这串信息,可以用git filter-repo这类工具重写历史;第三步是确认所有克隆过仓库的成员重新同步,防止旧历史继续传播。
这条经验我想多说两句:很多人以为“上线前体检”只要扫当前代码就够,但忽略了一个事实——只要仓库里出现过密钥,攻击者就多了一个入口。密钥轮换的成本并不高,但拖着不处理,风险是整个生产环境被拖下水。
3.5 第四轮:容器与部署配置体检
代码和依赖检查完,我把 Dockerfile 和镜像也放上台面。这个项目的基础镜像是python:3.9-slim,看似没问题,但用 Trivy 一扫,镜像里有多个高危和严重漏洞,分布在系统库和 Python 运行时里。原因很直接:基础镜像版本太老,且没有额外的加固层。
Dockerfile 里的第二个问题是容器以 root 用户运行。这意味着一旦应用被攻破,攻击者拿到的就是容器内的最高权限。修复时我把 Dockerfile 改成了多阶段构建,并在最终镜像里创建非 root 用户,加上健康检查:
FROM python:3.11-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt FROM python:3.11-slim RUN useradd --create-home appuser WORKDIR /app COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY . . USER appuser HEALTHCHECK CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"改完再扫一遍,高危和严重的数量大幅下降。配置层面的体检虽然不像代码漏洞那么“酷”,但它决定的是你的应用跑在一个什么样安全基线的环境里。
3.6 风险分级与上线建议
全部扫描结果汇聚之后,我按影响范围做了一个分级表,也把它当作上线决策的依据。
| 风险等级 | 问题示例 | 上线建议 |
|---|---|---|
| P0 | 硬编码密钥、SQL 注入、已泄露的云凭证 | 必须修复,不允许带病上线 |
| P1 | 路径穿越、弱随机数、root 运行容器 | 上线前修复或给出明确缓解方案 |
| P2 | 基础镜像低危漏洞、日志格式不规范 | 近期排期修复,可短期接受 |
| P3 | 低危问题、代码风格类告警 | 登记跟踪,持续优化 |
这个分级不是说“P2 以上就可以安心上线”,而是让我们在面对一堆告警时不会手足无措。先打掉 P0,再处理 P1,随后排期消化 P2,整个过程用一张表格盯住状态。上线前的焦虑感,很多都来自“不知道问题到底严重到什么程度”;分级,就是把未知变成已知。
4. 体检中的常见问题与排查技巧
工具能跑出来告警,但真正让体检产生价值的,是如何处理这些告警。在这几轮实操里面,有一些经验特别值得写下来,尤其是误报处理、漏洞升级困境、AI 幻觉依赖和 CI 集成这四个点。
4.1 扫描器误报多,如何快速“去噪”
第一次跑 Semgrep 或者 Bandit 的时候,告警数量会大得惊人。其中有一部分确实是误报,比如工具无法理解业务上下文,把“经过校验的用户输入”仍然标记成注入。不少团队被误报劝退,最后关了扫描器,这属于因噎废食。
我的处理办法是“先看分类,再看路径”。高危和中危问题逐个打开代码上下文做确认;低危和风格类告警先放一边,不做逐条解释。在 CI 里给扫描器配置基线,把短期内不修的告警记录在案,新引入的告警才阻断合并。这样既不会被误报淹没,也不会错过真正的新风险。
4.2 高危 CVE 暂时无法升级怎么办
依赖审计最让人头疼的一种情况是:报告出一个高危 CVE,但对应的修复版本还不存在,或者升级会破坏现有功能。这时候不能无脑升级,也不能当作看不见。
我的做法是多管齐下。先确认这个漏洞是否真的在代码运行路径上被调用,如果只是被安装但没用到,可以记录为低风险;再检查运行时环境能不能收紧权限,比如最小化网络暴露面、限制服务账号权限;最后明确风险登记表和升级时限,要求持续跟踪漏洞库更新。安全体检的目标从来不是“零漏洞”,而是“已知漏洞都被评估过、缓解过、跟踪过”。
4.3 AI 幻觉依赖与看似安全的“坏味道”
AI 生成代码时还有一种隐蔽风险:幻觉依赖。模型可能生成一个看起来及其合理的 import 语句,比如from nice_login import verify_token,但这个包在 PyPI 上根本不存在,或者存在一个拼写相近的恶意包。如果开发时没注意,直接跑安装命令,很可能把恶意代码装进项目里。
体检时我会专门做一轮“依赖真实性检查”。逐个核对 requirements 里的包名是否存在、版本号是否真实存在,并检查 lock 文件里是否锁定了版本。尤其是 AI 推荐的那些冷门库,宁可换成生态里成熟有维护的替代品,也不要图一时方便。依赖这块,安全工具能查出已知漏洞,但查不出“包名本身是假的”,这一步必须靠人。
4.4 把体检放进上线流程,而不是上线当天的仪式
一次性的安全体检,价值远不如把它固化到研发流程里。我最推荐的落地方式是在 CI/CD 里增加一个安全扫描阶段,每次合并代码请求时自动执行 SAST、密钥扫描和依赖审计,任何高危问题都直接阻断合并。下面是一个在 GitLab CI 里加安全扫描阶段的简版示例:
security-scan: stage: test script: - semgrep --config=auto . - gitleaks detect --source . - pip-audit rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'这样做的好处是,问题在代码评审阶段就暴露出来,而不是攒到上线前一天夜里集中爆发。我见过太多团队把“上线前体检”当成一个流程节点,结果变成上线当天救火;真正稳妥的做法是让体检持续发生,让每次代码变更都默认带着安全防线。
5. 结论:怎么判断 AI 代码能不能信
现在回到标题那个问题:AI 帮我写的代码,上线前到底能不能信?我的答案是,信不信不是靠感觉判断的,而是靠证据。这个证据就是一轮完整的代码安全体检,以及一套可以持续执行的上线准入标准。
5.1 我的五条上线准入标准
这五条标准是我在大量项目里沉淀下来的一套“门槛”,不满足任何一条我都不会同意让 AI 生成的代码直接上生产。
| 维度 | 准入标准 |
|---|---|
| 静态分析 | 无未修复的高危漏洞,高危项已有人工确认与缓解 |
| 依赖审计 | 直接依赖无公开高危 CVE,传递依赖有明确处理方案 |
| 密钥与敏感信息 | 当前工作区和 git 历史中不存在密钥、口令、连接串 |
| 部署配置 | 容器不以 root 运行,镜像无高危漏洞,基础配置规范 |
| 人工复核 | 至少一位没有参与 AI 提示工程的工程师完成 Code Review |
每条标准背后都对应着一类事故类型:硬编码密钥对应的是批量扫描后的账号接管;依赖漏洞对应的是被公开漏洞利用;git 历史泄漏对应的是内网权限扩散。五条都过了,我才肯说“可以上线”,也才敢拍胸脯说“这道题 AI 代码可信”。
5.2 让 AI 写代码,但不让 AI 替你负责
还有一个更深层的心得:AI 是很好的“执行者”,但不是安全“负责人”。我现在的使用姿势是让 AI 生成单个函数、写测试用例、解释陌生代码、给正则表达式,而不是把整个业务模块一股脑丢给它,然后期待它写得又快又稳。每次用 AI 生成代码,我都会在验收清单上过一遍:有没有涉及鉴权、有没有处理用户输入、有没有引入新依赖、有没有碰敏感数据。把这些点都确认过之后,AI 生成的代码才真正成为“我的代码”。
很多安全问题并不是 AI 一个人造成的,而是使用 AI 的人放弃了自己作为工程师的判断力。模型可以帮你省掉打字的体力活,但威胁建模、风险识别、方案评估这些核心能力,只能人自己来。
5.3 我自己的体会
最后说说我的个人经历。有一次我给一个回调接口做联调,AI 生成的代码跑起来很顺,逻辑也没问题,我当时急着上线,省略了完整体检,只做了基础功能验证。结果上线第二天,这个接口被脚本刷了几百万次请求,因为没有加频率限制和状态校验。那次之后,我给自己定了一条铁律:只要代码里有 AI 的痕迹,必须完整跑一遍体检流程再谈上线。
这份“固执”到现在帮我们避免了很多次潜在的线上事故。现在我看别人交给我的 AI 生成代码,第一反应不再是“这代码写得怎么样”,而是“这代码经过体检了吗”。工具会越来越强,AI 生成代码的比例还会继续上升,但“先体检、再上线”这件事,我觉得永远不会过时。