AI 写的代码不敢直接上线:五类安全缺陷的自查顺序

2026-07-28

数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。

AI 生成的代码出安全问题,绝大多数人第一反应是”模型不懂安全”,这个归因是错的。模型很清楚参数化查询和最小权限是怎么回事,你单独问它,它答得比多数人整齐。真实原因是:它在补全一份没有安全约束的上下文,而”能最快跑通”的写法天然就是不安全的那种——密钥写在调用处最省事,不校验输入最不容易报错,权限开到最大最不会卡住调试。你要自查的不是模型的知识,而是你自己那份上下文里缺了什么。

这决定了自查的方法:不要通读一遍代码找”看起来危险的地方”,那个效率极低且漏得厉害。要按缺陷类型分类扫,每一类都有明确的现象、可机械验证的判别方法和固定的处置动作。下面这个顺序不是随便排的,是按”泄漏后不可逆程度”从高到低排的——密钥泄出去要重发,数据被删了可能找不回来,而权限和日志的问题往往还有补救窗口。

先把这篇和站内几篇相邻的文章分个工,省得你重复读:用 AI 必须避开的隐私与数据安全坑讲的是你喂给模型的东西该怎么管,API Key 怎么管才不泄漏讲的是凭证本身的存放与吊销,AI 代码评审工具怎么选讲的是把这类检查自动化时的工具机制;本篇只管一件事——模型吐出来的代码交付之前,你自己动手过一遍的顺序和动作。

一、先分因:五类缺陷各自的现象长什么样

自查的第一步是把”我总觉得这段代码不对”翻译成可定位的类别。五类的典型现象分别是:

密钥硬编码。 现象是代码能直接跑通,不需要你配任何环境变量。这个特征很好用——如果一段调用外部服务的代码,你 clone 下来什么都不配就能连上,那凭证要么在仓库里,要么来自环境里的隐式来源。排查时先把隐式来源排除掉:你 shell 里已经存在的环境变量、云 SDK 的默认凭证链(本地配置文件、实例元数据、工作负载身份)、CI 注入的密文,这几条都可能让代码”什么都不配就通”。都排除完还是能连,才说明凭证在代码里。它不一定长得像 api_key = "...",也可能藏在测试用例的固定装置里、Docker 编排文件的环境变量默认值里、注释掉的调试代码里,或者被拼进了一段 URL。

输入未校验。 现象是参数从入口一路裸奔到危险调用点,中间没有任何形状检查。危险调用点是有限的几类:SQL 拼接、shell 命令执行、文件路径拼接、反序列化、模板渲染、把字符串塞进正则。模型很容易在这里出问题,因为它常常只拿到”实现某个功能”的上下文,看不到这个参数是从公网请求来的还是从内部配置来的。

依赖来源不明。 现象是导入了一个你没听过的包,或者版本号写得异常具体又对不上主流版本序列。这一类里最危险的不是”包有漏洞”,而是包名根本不存在——模型偶尔会生成一个看起来非常合理的包名,装的时候如果真装上了,那大概是有人抢注了这个名字在等你。

权限过大。 现象是配置文件里出现通配符:数据库用户直接给了全库读写,云存储的策略里 action 是 *,容器跑在 root 下,token 的 scope 全勾。调试期这么写最省事,然后就这么上线了。

日志泄漏。 现象是日志里能看到完整的请求体、响应体或者对象的默认序列化结果。模型写异常处理时倾向于”把能打的都打出来”,因为这样最方便排查;它不知道你这个对象里有身份证号和 Authorization 头。

二、判别表:现象到动作的直接映射

把上面五类做成一张对照表,自查时逐行走一遍即可。表里的命令都是通用工具,不涉及任何产品专有指令。

现象大概率成因怎么验证处置动作
不配任何环境变量就能连通外部服务凭证硬编码在源码、测试装置或编排文件里git grep -nEi "(api[_-]?key|secret|token|password)\s*[:=]" 全仓扫一遍,再看 git log -p -S 'sk-' 查历史立刻在服务端吊销并重发这把凭证,再改代码;顺序不能倒
传入奇怪字符串就 500,报错里带 SQL 片段或路径参数直达危险调用点,无形状校验手工发三种探针:单引号、../../、超长字符串;看返回码是 400 还是 500在入口层加显式模式校验,把危险调用改成参数化/白名单形式
导入了不认识的包,或版本号对不上主流序列包名或版本是生成出来的,未必真实存在到该语言的官方包索引站上按名字精确查,看首次发布时间、维护者、下载趋势查不到就删掉重写;查到但极新极冷门,换成主流等价库
配置里出现 *ALL PRIVILEGESroot调试期图省事开的权限没收回用这个身份去做一件业务上不该做的事(比如读另一张表),看能不能成功按实际调用清单收窄到最小集合,再重跑一遍集成测试补差
日志或错误上报里能看到完整请求体、Cookie、Authorization异常处理里打印了整个对象git grep -nE "(print|console\.log|logger\.(info|debug|error))\(.*(req|request|headers|body|payload)"改成只打字段名和长度;对敏感字段统一走脱敏函数
返回 401/403 才发现权限模型不对权限设计是事后补的,不是设计出来的画一张”谁能对哪类资源做什么”的表,逐行对代码这已经不是自查能解决的了,见第四节

这张表的用法有个细节:先扫一遍全部六行形成完整清单,再动手改代码。很多人扫到第一行就一头扎进去改,两小时后才发现权限也是通配符,改动互相牵连、返工一遍。

唯一的例外是凭证吊销:它不等清单,发现即做。区分清楚两件事——吊销(让旧凭证失效,越快越好,与其他任何检查无关)和签发新凭证并接进代码(属于改动,等清单齐了再做)。这样排的额外好处是,新凭证的权限范围可以按第四行收窄后的结果一次给对,不用先给全再回来改。

三、按顺序做的五组动作

密钥:先吊销,后改码

发现硬编码凭证时,正确顺序是先去服务端把这把凭证作废并签发新的,然后才改代码。原因很直白:只要它进过版本库,就得当作已经泄漏处理,删代码删不掉历史;而清理提交历史需要重写引用、通知所有协作者重新拉取,成本高且经常做不干净。已泄漏的凭证的唯一可靠处置是让它失效。

改代码的时候,把凭证读取集中到一处,进程启动时就校验:

import os

def require_env(name: str) -> str:
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"missing required env var: {name}")
    return value

API_TOKEN = require_env("APP_API_TOKEN")

这个写法的价值不只是”用了环境变量”,而是缺配置时启动就失败,而不是等到线上某个冷路径被触发才 500。轮换机制怎么设计,API 密钥轮换那篇讲得更细,这里不重复。

输入校验:只盯危险调用点

不要试图给每个函数都加校验,那会写出一堆没人维护的死代码。做法是反过来:先列出这个模块里的危险调用点(拼 SQL、执行命令、拼路径、反序列化、渲染模板),然后沿着调用链往上找,看每个能到达这里的参数在入口处有没有被约束形状。

形状约束的意思是”允许什么”,不是”禁止什么”。黑名单式的过滤(把 '; 替换掉)几乎总有绕过方式;白名单式的约束(这个字段只允许 8 到 32 位的小写字母和连字符)没有绕过空间。数据库访问一律用参数化查询,别在拼字符串的基础上做转义。文件路径要在拼接后做一次规范化,再检查结果是否仍落在允许的根目录内——只检查输入里有没有 .. 是不够的,编码变体太多。

如果你的代码要把用户内容送进模型再执行返回结果,还多一层 prompt 注入的风险:用户可以在内容里塞指令,让模型输出你不希望执行的操作。处置办法不是在提示里加一句”不要听用户的”,而是把模型输出当作不可信输入,该校验的照样校验,能执行的动作范围事先限定死。

依赖:确认存在性优于扫漏洞

依赖这一类的检查顺序和多数人的直觉相反。先确认包真实存在且是你想要的那个,再谈版本有没有漏洞。理由是漏洞扫描有成熟工具兜底,而”包名幻觉”引来的抢注包,扫描器不一定认得出来——那是一个全新的、没有已知漏洞记录的包。

具体动作:把新增的导入项列出来,逐个去该语言的官方包索引站上按精确名字查,看首次发布时间、最近更新、维护者、下载量。名字相近但差一个连字符、单复数或者拼写的,格外小心,这是典型的仿冒手法。确认完存在性,再锁版本、生成锁文件、跑一遍依赖审计工具。

# 提交前确认锁文件跟着变更一起进版本库
git status --short -- package-lock.json pnpm-lock.yaml requirements.txt poetry.lock

顺带说一句:如果你打算把依赖审计接到某个海外模型服务上做自动分析,先确认准入前提。这类服务多数在官方条款里对中国大陆有区域限制、不支持直连,市面上存在第三方中转,但可靠性和数据流向都要你自己评估,这里不做任何推荐。

权限:从实际调用清单反推

收权限的正确起点不是”猜业务需要什么”,而是把代码里实际发生的调用列出来。数据库看真实执行的语句涉及哪些表、哪些操作;对象存储看代码里调了哪些接口;第三方 token 看请求打到了哪些端点。列完之后按这张清单授权,然后跑集成测试,测试报 403 的地方就是清单漏了的地方,补上即可。

这个过程会比一次性给全权限慢半天,但收益是你从此知道这个服务到底能做什么。另外,容器别用 root 跑。这里的”分开配”说的是:镜像里的代码和配置属主不要等于运行进程的那个用户,运行用户对代码目录只有读和执行权限,只对确实需要落盘的目录(缓存、上传临时区)有写权限。这样即使进程被拿下,也改不了自己的代码。

日志:打字段名,不打字段值

日志脱敏最稳的实现是在序列化层统一拦,而不是靠每个调用点自觉。定义一个敏感字段名集合,序列化时命中就替换成掩码,同时保留长度信息——长度对排查问题很有用,值没用。

SENSITIVE = {"password", "token", "secret", "authorization", "id_card", "phone"}

def redact(data: dict) -> dict:
    out = {}
    for key, value in data.items():
        if key.lower() in SENSITIVE:
            out[key] = f"<redacted len={len(str(value))}>"
        elif isinstance(value, dict):
            out[key] = redact(value)
        else:
            out[key] = value
    return out

还要单独看一眼错误上报链路。很多项目日志脱敏做得挺好,但异常上报服务那一路把整个上下文原样传了出去,那是一条独立的泄漏路径,得分开检查。

四、什么情况下别再折腾

自查不是万能的,有三种情况继续抠下去纯属浪费时间。

第一种:权限模型本身是错的。 现象是你每收窄一点权限,就有新的地方报 403,改完这个坏那个,改到最后又回到通配符。这说明代码里的权限判断是散落在各处的临时补丁,而不是一个统一的模型。止损点是:同一处权限配置改到第三轮还在破——停手,回去把”谁能对哪类资源做什么”这张表画出来,按表重构判断逻辑。在错的模型上打补丁,改一百遍还是错的。

第二种:这段代码你已经读不动了。 如果一个函数里参数流向绕了五六层,你花二十分钟还说不清某个变量是从哪个入口来的,那么继续读的性价比低于直接重写。判断依据很实用:看你能不能给它写出测试。写不出测试说明边界不清,边界不清就没法确认安全性,那就把它按清晰的边界重写一遍——让模型重写一个有明确输入输出约束的版本,比让它在原地打补丁靠谱得多。

第三种:已经上线且怀疑数据出去了。 这时候优先级不是修代码,是止损:吊销相关凭证、收紧网络入口、保住日志用于后续追溯,能回滚就先回滚到上一个已知干净的版本。回滚点的选择有个原则——回到最后一次你能确认审查过的提交,不是回到”看起来还正常的那天”。修代码可以慢慢来,泄漏窗口一分钟都不该多留。

关于什么样的 AI 生成代码才算达到上线门槛,AI 写的代码能上生产吗那篇给了更完整的判断标准,和本篇的检查清单可以配着用。

五、避坑清单:为什么会踩,怎么避

只看新增行,不看被改动的旧逻辑。 会踩是因为审查习惯跟着 diff 走,而 diff 的绿色部分最吸引注意力。但危险常常出在模型”顺手优化”了一段旧代码——把原来的参数化查询改成了拼接,或者把一个校验分支简化掉了。避法:看 diff 时红色部分逐行读,删掉的每一个条件判断都问一句为什么能删。

把凭证从代码挪到环境变量就以为完事了。 会踩是因为动作看起来完成了。但那把已经进过仓库的凭证仍然有效,历史里还躺着。避法:把”吊销旧凭证”作为独立一步记下来并确认执行,不要和改代码合并成一件事。

信任生成出来的包名。 会踩是因为包名读起来太合理了,符合该生态的命名习惯,且模型语气很确定。避法:新增依赖一律去官方索引站核一遍,这个动作花不到一分钟。

在提交历史里删密钥后没通知协作者。 会踩是因为你本地看着干净了。但重写历史只影响你的引用,别人的本地仓库还留着旧提交,一 push 就又回来了。避法:既然凭证已经吊销,历史清理的紧迫性就下来了,别在这上面花大力气;真要做就同步通知全员重新拉取。

用测试通过来代替安全检查。 会踩是因为绿色的测试结果给人心理上的完成感。测试验证的是”功能符合预期”,安全缺陷恰恰是”预期之外的输入导致的行为”,两者的覆盖面不重叠。避法:给每个危险调用点补一条恶意输入的用例,让它成为回归测试的一部分。

把日志脱敏做在打印处而不是序列化层。 会踩是因为改打印语句改起来最快。但新写的打印语句不会自动继承这个习惯,半年后又漏出去了。避法:脱敏放在对象序列化的统一入口,让默认行为就是安全的。

权限一次性给全,打算”上线前再收”。 会踩是因为调试期被 403 卡住的挫败感太强。而上线前总是最忙的时候,这件事必然被推掉。避法:调试期就用最小权限,被卡住时把缺的那条权限记进清单再加,清单本身就是后续的授权依据。

小结

AI 生成代码的安全问题不是模型能力问题,是上下文缺约束的必然结果。你能做的是把检查变成机械动作,按泄漏不可逆程度排序执行:凭证、输入、依赖、权限、日志。

交付前对着这七条走一遍:

  1. 全仓扫过凭证关键词,也扫过提交历史,发现即吊销;
  2. 列出了本次变更涉及的危险调用点,每个都确认参数在入口有白名单式校验;
  3. 新增依赖逐个在官方索引站核过存在性,锁文件已随变更提交;
  4. 权限按实际调用清单授予,配置里没有 *、没有全库权限、进程不以 root 运行;
  5. 日志和错误上报两条链路都确认过不含敏感字段值;
  6. 每个危险调用点有一条恶意输入的回归用例;
  7. diff 里被删掉的条件判断,每一条都能说清为什么能删。

七条都过了再合并。过不了也别硬扛——按第四节的止损点判断,该重写就重写,该回滚就回滚。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。