1. 从“看代码对不对”到“看边界在哪”:一次认知升级
前阵子接手了一个内部工具项目,核心逻辑是用大模型批量生成数据校验脚本。项目不大,但踩的坑足够写满两页纸。最让我后背发凉的一次,是生成的代码在测试环境跑得漂漂亮亮,上线后却因为一个没被约束的输入参数,把整张配置表刷成了默认值。事后复盘,问题根本不在“代码写得对不对”——语法没毛病,逻辑也自洽,问题出在我压根没想过“这段代码的信任边界在哪里”。
这件事之后,我把手头所有涉及 AI 生成代码的项目重新过了一遍,慢慢总结出一套自己的审查方法。核心就一句话:不要只盯着代码本身,要盯着代码与外部世界之间的那几条信任边界。AI 生成的代码有个特点,它在“局部正确性”上往往表现很好,函数写得干净、命名规范、注释齐全,但它对“全局约束”和“边界条件”的理解是缺失的,因为它看到的只是你给它的那段上下文,看不到你的系统全貌、数据流向和真实调用场景。
这篇文章想聊的就是这套审查思路。适合谁看?如果你正在用 AI 辅助写代码,不管是写业务逻辑、写脚本、写配置,还是做 AI 应用开发,只要你把 AI 生成的代码往生产环境里放,这套东西就用得上。我不打算讲什么高深的安全理论,就讲三条我实际用下来最有效的信任边界,以及每条边界上具体怎么查、查什么、查到问题怎么改。
先说清楚一个前提:AI 生成代码的安全审查,和传统代码审计不是一回事。传统审计假设代码是人写的,人会犯困、会偷懒、会理解偏差,但人对自己写的系统有整体认知。AI 不一样,它没有系统认知,它是在概率上“猜”你想要什么。所以你审查的重点也要跟着变——从“这段逻辑对不对”转向“这段代码假设了什么、依赖了什么、暴露了什么”。
2. 第一条信任边界:输入边界
2.1 为什么输入边界是 AI 代码最脆弱的地方
AI 生成代码时,对输入的处理往往是最随意的。原因很简单:你在提示词里描述需求时,通常只会说“写一个函数,接收用户 ID,返回用户信息”,你不会说“这个用户 ID 可能为空、可能是负数、可能是超长字符串、可能包含 SQL 注入片段”。AI 就默认输入是“正常”的,它按最理想的路径去写。
我见过最典型的一个例子:让 AI 写一个根据 ID 删除记录的接口,它生成的代码大概长这样:
def delete_record(record_id): db.execute(f"DELETE FROM records WHERE id = {record_id}") return {"status": "ok"}语法没问题,逻辑也通,但这里有三层输入边界完全没设防:第一,record_id没有类型校验,传个字符串进来直接拼接;第二,没有权限校验,任何人传任何 ID 都能删;第三,没有软删除或确认机制,删了就真没了。AI 不会主动帮你加这些,因为你的提示词里没提。
2.2 输入边界审查清单
我现在审查 AI 生成的代码,输入边界这块固定查这几项:
- 类型与格式校验:所有外部传入的参数,是否做了类型检查、格式校验、长度限制。特别是数字型参数,有没有做范围约束。
- 空值与默认值处理:参数为空时行为是什么?是报错、是走默认值、还是静默跳过?AI 经常在这里留坑,比如空字符串被当成有效值处理。
- 注入类风险:凡是拼接 SQL、拼接命令、拼接路径的地方,必须检查是否有转义或参数化处理。AI 生成字符串拼接的概率远高于生成参数化查询。
- 权限与身份:这个输入是否携带了调用者身份?代码有没有校验“当前用户是否有权操作这个资源”?AI 默认不写权限逻辑。
- 频率与容量:输入是否可能被高频调用?有没有限流或容量保护?AI 生成的代码通常假设“一次只来一个请求”。
提示:审查输入边界时,不要只看函数签名,要顺着调用链往上找。AI 生成的函数可能自己做了校验,但调用它的地方可能绕过了校验直接传参。
2.3 一个真实的排查过程
前面说的那个“配置表被刷”的事故,根因就在输入边界。AI 生成的更新函数接收一个字典参数,直接遍历字典的 key 去更新对应字段。测试时传的字典只有两三个 key,没问题。上线后上游系统传了一个包含几十个 key 的字典,其中大部分是空值,结果把配置表里原本有值的字段全覆盖成了空。
排查时我做了三件事:第一,把函数签名和实际调用处的参数打印出来对比,发现调用处传的字典比预期大得多;第二,检查函数内部有没有对 key 做白名单过滤,发现没有;第三,检查空值处理逻辑,发现 AI 把“key 存在”等同于“需要更新”,没有区分“显式传空”和“未传”。
修复方案也很直接:加一层 key 白名单,只允许更新预定义的字段;对空值做区分处理,未传的 key 不更新,显式传空的 key 才更新为空。这两条加进去之后,同类问题再没出现过。
3. 第二条信任边界:执行边界
3.1 执行边界到底在防什么
执行边界管的是“代码在运行过程中,能碰到什么、能改什么、能调什么”。AI 生成的代码在执行层面有几个高频问题:一是资源没释放,比如打开的文件、数据库连接、网络请求没有正确关闭;二是异常没兜住,一个环节出错整个流程崩掉;三是副作用没控制,比如在查询函数里偷偷写了更新操作。
我印象很深的一次,是让 AI 写一个批量处理数据的脚本。它生成的代码逻辑很清晰:读文件、逐行处理、写结果。但问题在于,它把整个文件读进内存再处理,而且没有做分批。测试数据只有几千行,跑得飞快。实际数据是几百万行,直接内存溢出。这就是典型的执行边界问题——AI 不知道你的数据规模,它按“小数据”的假设去写。
3.2 执行边界审查要点
执行边界这块,我固定查这几个维度:
| 审查维度 | 具体检查项 | AI 常见问题 |
|---|---|---|
| 资源管理 | 文件、连接、锁是否成对出现 | 打开不关闭,加锁不解锁 |
| 异常处理 | 关键路径是否有 try-catch | 异常直接往上抛,或吞掉不报 |
| 事务边界 | 多步操作是否在同一事务内 | 部分成功部分失败,数据不一致 |
| 副作用控制 | 查询函数是否修改了状态 | 读操作里藏写操作 |
| 性能假设 | 是否假设了数据规模 | 全量加载、循环内查询 |
| 并发安全 | 共享资源是否有并发保护 | 多线程下竞态条件 |
这张表是我踩坑踩出来的。每一条背后都有至少一次线上事故或者差点上线的事故。比如“循环内查询”这条,AI 特别容易生成在 for 循环里逐条查数据库的代码,数据量小的时候看不出来,数据量一大就是灾难。
3.3 执行边界的实操检查方法
光看代码有时候看不出来执行边界的问题,我一般会配合几个动作:
第一,构造边界数据跑一遍。不看正常数据,专门看空数据、超大数据的表现。AI 生成的代码在正常数据下通常没问题,问题都出在边界上。
第二,看异常路径。把依赖的服务停掉、把数据库连接断开、把文件权限改掉,看代码怎么反应。AI 生成的代码在异常路径上的处理往往很粗糙。
第三,看资源监控。跑的时候盯着内存、连接数、文件句柄数,看有没有持续增长不释放的。这个最直观,AI 生成的代码如果有资源泄漏,跑一会儿就能看出来。
注意:执行边界的审查一定要在接近真实的环境里做。本地开发环境跑得再顺,也不能说明执行边界没问题,因为本地环境的资源约束和真实环境完全不一样。
4. 第三条信任边界:输出边界
4.1 输出边界为什么容易被忽略
输入边界和执行边界大家多少会注意,输出边界是最容易被忽略的。因为直觉上会觉得“输出是我自己控制的,能有什么问题”。但 AI 生成的代码在输出层面同样有坑,而且这些坑往往更隐蔽。
最常见的输出边界问题是信息泄漏。AI 生成的错误处理代码,经常把内部细节直接暴露给调用方。比如数据库报错,它直接把原始错误信息返回,里面可能包含表名、字段名、甚至连接字符串。再比如用户查询接口,它把整个用户对象返回,里面可能包含密码哈希、内部备注等不该暴露的字段。
另一个高频问题是输出格式不稳定。AI 生成的代码在不同条件下返回的数据结构可能不一致,有时候返回列表,有时候返回单个对象,有时候返回 null。调用方如果按固定格式解析,就会出问题。
4.2 输出边界审查清单
输出边界我固定查这几项:
- 敏感字段过滤:返回的数据里有没有不该返回的字段?特别是用户对象、配置对象、日志对象。
- 错误信息脱敏:异常信息里有没有暴露内部路径、表名、堆栈?对外返回的错误信息应该是通用的。
- 格式一致性:成功和失败时返回的结构是否一致?空结果和正常结果的结构是否一致?
- 数据量控制:返回的数据量有没有上限?会不会因为查询条件宽松返回海量数据?
- 编码与转义:输出到前端的内容有没有做转义?有没有 XSS 风险?
4.3 输出边界的修复思路
输出边界的修复通常比输入和执行边界简单,因为改动点集中。我的做法是加一层“输出过滤器”,所有对外返回的数据都经过这层过滤。具体来说:
对于敏感字段,定义一个白名单,只允许白名单里的字段通过。对于错误信息,统一包装成标准格式,内部错误记日志,对外只返回通用提示。对于格式一致性,定义一个统一的响应结构,成功失败都走同一个壳。对于数据量,加一个默认上限,超过就截断或报错。
这层过滤器加进去之后,输出边界的问题基本就堵住了。而且这层过滤器可以复用,所有接口都走同一套逻辑,维护成本很低。
5. 三条边界之外:AI 代码审查的通用心法
5.1 不要问“代码对不对”,要问“代码假设了什么”
这是我这套方法的核心。AI 生成的代码,你问它“对不对”,它大概率是对的,因为它在局部逻辑上确实自洽。但你要问它“假设了什么”,就能挖出问题。它假设输入是正常的、假设数据量是小的、假设调用方是可信的、假设依赖是稳定的。这些假设在真实环境里往往不成立。
我现在的习惯是,拿到 AI 生成的代码,先不跑,先读一遍,把它的隐含假设列出来。然后逐条问:这个假设成立吗?如果不成立会怎样?这样一轮下来,大部分问题都能提前发现。
5.2 审查要跟着数据流走
三条边界不是孤立的,它们是一条数据流上的三个关卡。输入边界管数据进来,执行边界管数据在系统内怎么流转,输出边界管数据出去。审查的时候要顺着数据流走一遍,看数据在每个关卡上有没有被正确处理。
我一般会画一个简单的数据流图,标出数据从哪来、经过哪些处理、到哪去。然后在每个边界上标注检查项。这样审查起来不会漏,也不会重复。
5.3 把审查经验沉淀成检查清单
每次踩坑之后,我都会把问题记下来,归类到三条边界里,更新我的检查清单。时间长了,这份清单就成了我审查 AI 代码的“作弊条”。现在拿到 AI 生成的代码,我基本是照着清单过一遍,效率比从头读代码高得多。
这份清单我也建议你自己维护一份。因为每个人的技术栈、业务场景、风险偏好不一样,通用的清单只能覆盖共性问题,真正高频的坑往往和你的具体场景相关。自己踩过的坑,记下来,下次就不会再踩。
6. 常见问题与排查技巧实录
6.1 AI 生成的代码在测试环境没问题,上线就出问题,怎么排查
这是最典型的情况。排查思路是:先对比测试环境和生产环境的差异,重点看数据规模、并发量、依赖服务的响应行为。然后按三条边界逐一排查:输入边界看生产环境的真实输入是不是比测试环境复杂;执行边界看生产环境的资源约束是不是更紧;输出边界看生产环境的调用方是不是对输出格式有更严格的要求。
我遇到过的几次这类问题,根因分别是:输入数据里多了测试环境没有的特殊字符、执行时并发量上来之后出现了竞态、输出数据量超过调用方的解析上限。都不是代码逻辑本身的问题,都是边界问题。
6.2 怎么判断 AI 生成的代码有没有隐藏的副作用
看函数名和实际行为是否一致。如果函数名叫get_xxx或者query_xxx,但里面出现了update、insert、delete、write这类操作,就要警惕。AI 有时候会把“查询并更新缓存”这种逻辑写在一个看起来像纯查询的函数里。
另一个方法是看函数的参数。如果参数里有db、session、connection这类对象,说明这个函数有执行能力,不只是一个纯计算函数。有执行能力就有副作用风险,要重点看。
6.3 AI 生成的代码里,哪些地方最容易出安全问题
按我的经验,高频出问题的地方依次是:字符串拼接(SQL、命令、路径)、异常处理(吞异常、暴露内部信息)、权限校验(缺失或写错)、资源管理(不释放)、并发控制(没有锁或锁用错)。这几个地方我基本是逐行看的,不敢跳。
6.4 审查 AI 代码时,有没有什么工具可以辅助
工具能帮上忙,但不能替代人工审查。静态扫描工具可以查出一些明显的注入风险、资源泄漏,但对逻辑层面的边界问题基本无能为力。我的做法是工具扫一遍,把明显的问题过滤掉,然后人工重点看三条边界。工具负责“查错”,人负责“查边界”,分工明确。
6.5 怎么让 AI 生成更安全的代码
提示词里明确写清楚边界要求。不要只说“写一个删除接口”,要说“写一个删除接口,需要校验调用者权限、需要校验 ID 格式、需要软删除、需要记录操作日志”。你把边界要求写进去,AI 生成出来的代码就会带上这些逻辑。虽然不能完全依赖它,但至少能减少后续审查的工作量。
7. 我个人的几条实操心得
第一条,AI 生成的代码,第一遍不要跑,先读。跑起来没问题不代表没问题,很多边界问题在正常数据下根本触发不了。先读一遍,把三条边界过一遍,心里有数了再跑。
第二条,审查的重点放在“变化”上。AI 生成的代码和你手写的代码,最大的区别在于 AI 不知道你的系统在变化。数据在增长、调用方在增加、依赖在升级,这些变化 AI 都感知不到。所以审查时要特别关注那些“假设了不变”的地方。
第三条,把边界检查做成自动化。三条边界里的很多检查项是可以自动化的,比如输入参数的类型校验、输出字段的白名单过滤、资源释放的成对检查。能自动化的就自动化,人工只负责那些需要判断力的部分。
第四条,不要追求一次审查到位。AI 代码的审查是一个持续的过程,第一遍查出明显问题,上线后根据实际表现再补。关键是建立一套可复用的审查框架,每次审查都按框架走,不靠灵感。
这套方法我用了大半年,经手的 AI 生成代码少说也有几万行,线上再没出过因为边界问题导致的事故。当然,这不代表 AI 生成的代码就安全了,只是说在“边界”这个维度上,我有了可控的审查手段。至于更深层的逻辑正确性、业务一致性,那又是另一个话题了。