1. AI 写代码这件事,真正的风险藏在哪
这两年我身边几乎所有开发团队都在用 AI 辅助写代码,效率提升是实打实的,但踩的坑也是实打实的。我见过太多团队把 AI 生成的代码直接合进主分支,测试环境跑得挺欢,上线之后被一个畸形参数打穿服务的事故。问题出在哪?绝大多数时候不是 AI 写的业务逻辑有多离谱,而是它写代码时默认"调用方会规规矩矩传参"——这个假设在真实世界里根本不成立。
AI 写代码有一个非常鲜明的特征:它擅长把"正常路径"写得又快又漂亮,但对"异常路径"的覆盖往往是缺失的。你让它写一个用户注册接口,它能把参数校验、密码加密、数据库写入、返回结构安排得明明白白,但如果你不专门提示,它很少会主动去想"如果有人传一个 10MB 的字符串进来会怎样""如果数组里塞了 100 万个元素会怎样""如果字段类型是对象而不是字符串会怎样"。这些恰恰是攻击者最先尝试的地方。
所以这篇内容我想聊的核心就一件事:在系统的"边界"处把输入验证做扎实。所谓边界,就是外部数据第一次进入你系统的那个位置——HTTP 接口的入参、消息队列的消费入口、文件上传的解析点、第三方回调的接收端。这些位置是攻击面最集中的地方,也是 AI 生成代码最容易偷懒的地方。不管你是刚接触 AI 编程的新手,还是已经用 AI 写了半年代码的老手,只要你的系统对外暴露了接口,这篇内容里的思路和清单都能直接拿去用。
我下面会从设计思路、核心细节、实操落地、问题排查四个层面展开,尽量把"为什么这么做"讲透,而不是只丢给你一堆规则。因为规则会过时,但判断力不会。
2. 整体设计思路:为什么边界验证要单独拎出来做
2.1 边界验证的本质是"不信任"
先建立一个心智模型:你的系统内部可以互相信任,但边界处必须默认一切输入都是恶意的。这个原则听起来简单,但落地时很多人会打折扣。比如有的团队觉得"这个接口只有内部服务调用,不用校验",结果内部服务被攻破之后,攻击者就顺着这个"信任通道"长驱直入。还有人觉得"前端已经校验过了,后端就不用重复校验",但前端校验只是用户体验优化,攻击者用 curl 直接打你的接口,前端那层校验形同虚设。
AI 生成代码时,如果你给的提示词里没有明确要求"对所有外部输入做严格校验",它大概率只会做最基本的非空判断。我实测过很多次,让 AI 写一个接收 JSON 的接口,它给出的代码里往往只有if not data.get('name')这种级别的检查,类型、长度、格式、范围统统不管。这不是 AI 笨,而是它按照"最常见、最简洁"的模式在生成,而安全校验属于"额外负担",需要你显式要求。
2.2 为什么选择"集中式边界层"而不是"散落式校验"
校验代码的摆放位置有两种思路:一种是散落在每个业务函数里,用到哪个字段就校验哪个;另一种是在边界处集中做一层校验,业务层拿到的数据已经是"干净"的。我强烈推荐后者,原因有三个。
第一,可维护性。校验规则集中在一处,改规则的时候不用满项目找。我见过一个项目,手机号校验规则散落在 7 个文件里,后来运营商号段更新,改漏了一个,导致部分用户注册失败,排查了大半天。
第二,一致性。集中式校验能保证同一个字段在所有入口的校验标准一致。散落式校验很容易出现"A 接口校验了长度,B 接口忘了校验"这种漏洞,而攻击者最喜欢找的就是这种不一致的地方。
第三,AI 友好。当你用 AI 生成代码时,如果项目里已经有一个清晰的校验层,AI 在生成新接口时会倾向于复用这个模式;反之如果校验是散落的,AI 根本不知道该在哪里加校验,很容易漏掉。这一点是我用 AI 协作开发大半年后最深的体会——你给 AI 的代码结构越清晰,它生成的代码质量越高。
2.3 校验的四个维度:类型、范围、格式、语义
很多人做输入验证只做"类型检查",这远远不够。一个完整的边界校验应该覆盖四个维度,我把它总结成一张表,你可以对照检查自己的接口。
| 维度 | 检查内容 | 典型漏洞 | 示例 |
|---|---|---|---|
| 类型 | 字段的数据类型是否符合预期 | 类型混淆导致逻辑绕过 | 期望字符串却传入对象 |
| 范围 | 数值大小、字符串长度、数组元素个数 | 资源耗尽、缓冲区问题 | 传入 10 万长度的数组 |
| 格式 | 是否符合特定模式 | 注入类问题 | 邮箱、URL、日期格式 |
| 语义 | 业务上是否合理 | 越权、逻辑漏洞 | 转账金额为负数 |
这四个维度里,前三个是通用的,任何接口都该做;第四个是业务相关的,需要结合具体场景。AI 生成代码时,前三个维度如果你不提示,它基本不会主动做;第四个维度更是完全依赖你的业务知识,AI 帮不上忙。
2.4 校验失败后的处理策略
校验失败之后怎么处理,也是一个需要提前设计的问题。我见过两种极端:一种是校验失败直接抛异常,把堆栈信息返回给调用方,这等于把系统内部结构暴露给攻击者;另一种是校验失败静默忽略,继续往下走,这更危险,因为脏数据已经进入系统了。
正确的做法是:校验失败时返回统一的、不包含内部细节的错误响应,同时记录详细的日志供排查。错误响应里只告诉调用方"参数不合法",不要告诉它"哪个字段的哪个规则不满足"——虽然这对调试不太友好,但能避免给攻击者提供探测信息。日志里则要记录完整的原始输入、失败原因、请求来源,方便事后分析。
3. 核心细节解析:AI 生成代码最容易漏掉的校验点
3.1 类型校验:别被动态类型语言坑了
如果你用的是 Python、JavaScript 这类动态类型语言,类型校验是重中之重。AI 生成的代码经常直接假设字段是某个类型,然后调用该类型的方法。比如它写name.upper(),但如果调用方传进来的是数字,这行代码直接报错;如果传进来的是对象,可能触发更诡异的行为。
我踩过的一个真实坑:一个接口接收tags字段,AI 生成的代码里直接for tag in tags遍历。正常情况下 tags 是数组,没问题。但攻击者传了一个字符串"abc",遍历就变成了逐字符循环;传了一个字典,遍历就变成了遍历 key。虽然不一定造成严重后果,但这种"输入类型决定行为"的代码是典型的隐患。
正确的做法是在边界处做严格的类型断言。Python 里可以用 Pydantic,JavaScript 里可以用 Zod 或 Joi,这些库能帮你把类型校验做得又严谨又省事。下面是一个 Pydantic 的例子,你可以直接参考:
from pydantic import BaseModel, Field, validator from typing import List class CreateOrderRequest(BaseModel): user_id: int = Field(..., gt=0) product_ids: List[int] = Field(..., min_items=1, max_items=50) remark: str = Field("", max_length=200) @validator('product_ids') def check_product_ids(cls, v): if len(set(v)) != len(v): raise ValueError('product_ids contains duplicates') return v这段代码里,user_id必须是大于 0 的整数,product_ids必须是 1 到 50 个元素的整数数组,remark长度不超过 200。这些约束在数据进入业务逻辑之前就全部生效了。如果你让 AI 生成接口代码,可以在提示词里明确说"使用 Pydantic 做入参校验,覆盖类型、范围、格式",它生成的代码质量会高很多。
3.2 长度与数量限制:防资源耗尽的第一道墙
长度和数量限制是最容易被忽略、但性价比最高的校验。为什么?因为很多攻击的本质就是"用超大的输入耗尽你的资源"。一个字符串字段如果不限长度,攻击者传 100MB 进来,你的内存直接爆掉;一个数组字段如果不限元素个数,攻击者传 100 万个元素,你的循环能跑到天荒地老。
AI 生成代码时几乎不会主动加长度限制,除非你明确要求。我建议对所有字符串字段都设一个合理的最大长度,对所有数组字段都设最大元素个数。这个"合理"怎么定?看业务。用户名一般不超过 50 字符,备注不超过 500 字符,一次批量操作不超过 100 条。定这些数字的时候宁可紧一点,后面业务需要再放宽,也比一开始不限制要好。
还有一个容易被忽略的点:嵌套结构的深度限制。如果你的接口接收任意 JSON,攻击者可以构造一个深度嵌套的结构(比如 1000 层嵌套的对象),解析的时候可能触发栈溢出。Python 的 json 库默认有递归深度限制,但如果你用的是其他解析器,需要自己确认。
3.3 格式校验:正则不是万能的,但没有正则万万不能
格式校验主要针对有明确模式的字段,比如邮箱、手机号、URL、日期、UUID 等。AI 生成代码时,如果你说"校验邮箱格式",它通常会给你一个正则。但这里有个坑:AI 给的邮箱正则往往是简化版,覆盖不全或者过于宽松。
我的建议是:能用成熟库就用成熟库,别自己写正则。Python 里校验邮箱可以用email-validator,校验 URL 可以用validators,校验日期可以用dateutil。这些库经过大量实践检验,比 AI 随手写的正则靠谱得多。如果确实需要自己写正则,一定要做边界测试,比如邮箱正则要能正确处理a@b.co、a.b+c@d.com这些情况,也要能拒绝a@b、@b.com这些非法格式。
另外提醒一点:正则本身也可能成为攻击面。某些正则存在"灾难性回溯"问题,攻击者构造特定输入能让正则匹配耗时指数级增长,从而拖垮服务。AI 生成的正则如果包含嵌套的量词(比如(a+)+),就有这个风险。如果你不确定,可以用re2这类不支持回溯的正则引擎,或者对正则匹配设置超时。
3.4 语义校验:AI 帮不了你的部分
前面说的类型、范围、格式,都是通用规则,AI 多少能帮上忙。但语义校验完全依赖业务知识,AI 基本无能为力。什么是语义校验?就是"这个值在业务上是否合理"。
举个例子:一个转账接口接收amount字段,类型是数字,范围是正数,格式没问题。但语义上,转账金额不能超过账户余额,不能是 0.0001 这种精度异常的值,不能是 999999999 这种明显异常的大额。这些规则 AI 不知道,只有你清楚。
再比如:一个查询接口接收order_id,类型是字符串,格式是 UUID。但语义上,这个订单必须属于当前登录用户,否则就是越权访问。这个校验必须在业务层做,而且必须做,否则就是经典的越权漏洞。
我的经验是:把语义校验和边界校验分开。边界校验在入口处做,保证数据"形式上合法";语义校验在业务层做,保证数据"业务上合理"。两者不能互相替代。AI 生成代码时,边界校验你可以要求它做,语义校验必须你自己补。
4. 实操落地:从零搭建一个边界校验层
4.1 第一步:梳理所有外部入口
动手写代码之前,先做一件事:把所有外部数据入口列出来。这一步很多人会跳过,直接开始写校验,结果漏掉了某个入口。我建议用一张表来梳理,包含入口类型、位置、数据格式、敏感级别。
| 入口类型 | 位置 | 数据格式 | 敏感级别 |
|---|---|---|---|
| HTTP 接口 | /api/v1/orders | JSON | 高 |
| 消息队列 | order.created 主题 | JSON | 中 |
| 文件上传 | /api/v1/upload | 二进制 | 高 |
| 第三方回调 | /callback/payment | JSON | 高 |
| 定时任务参数 | 配置文件 | YAML | 低 |
梳理完之后,你会发现有些入口你可能之前根本没意识到是"外部入口"。比如配置文件,如果它是由运维手动编辑的,那也算外部输入,也需要校验。再比如环境变量,如果部署流程不规范,也可能被注入异常值。
4.2 第二步:定义校验规则并集中管理
梳理完入口,接下来定义每个入口的校验规则。我推荐用声明式的方式定义规则,而不是写一堆 if-else。声明式的好处是规则清晰、易于审查、方便 AI 理解和复用。
以 HTTP 接口为例,用 Pydantic 定义请求模型就是声明式的。每个字段的类型、范围、格式都在模型里声明清楚,校验逻辑自动生效。下面是一个更完整的例子,包含了常见的校验场景:
from pydantic import BaseModel, Field, EmailStr, HttpUrl, validator from typing import Optional, List from datetime import datetime class UserProfileUpdate(BaseModel): nickname: str = Field(..., min_length=1, max_length=30) email: Optional[EmailStr] = None homepage: Optional[HttpUrl] = None birthday: Optional[datetime] = None tags: List[str] = Field(default_factory=list, max_items=10) @validator('tags', each_item=True) def check_tag(cls, v): if len(v) > 20: raise ValueError('tag too long') if not v.strip(): raise ValueError('tag cannot be blank') return v.strip() @validator('birthday') def check_birthday(cls, v): if v and v > datetime.now(): raise ValueError('birthday cannot be in the future') return v这段代码覆盖了长度、格式、数量、语义四个维度的校验。注意tags的校验用了each_item=True,对数组里每个元素都做检查,这是很多人会漏的地方——只检查了数组长度,没检查元素内容。
4.3 第三步:在边界处统一拦截
规则定义好了,接下来要在边界处统一拦截。以 Web 框架为例,FastAPI 会自动用 Pydantic 模型校验入参,校验失败返回 422。但 422 的默认响应会暴露字段名和失败原因,如果你不想暴露这些信息,需要自定义异常处理器。
from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse app = FastAPI() @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): # 记录详细日志供排查 logger.warning(f"validation failed: {exc.errors()}, body: {await request.body()}") # 返回统一的不含细节的响应 return JSONResponse( status_code=400, content={"code": "INVALID_PARAM", "message": "request parameter invalid"} )这个处理器做了两件事:日志里记录完整信息,响应里只返回笼统的错误。这样既方便排查,又不给攻击者提供探测信息。如果你用的是其他框架,思路是一样的:找到框架的校验失败处理入口,替换成自己的逻辑。
对于消息队列、文件上传这些非 HTTP 入口,没有框架帮你自动校验,需要你自己在消费逻辑的最前面加一层校验。我的做法是写一个通用的校验装饰器,所有消费函数都套上,确保不会漏。
4.4 第四步:用 AI 生成校验代码的正确姿势
既然这篇内容的核心是 AI 写代码,那必须聊聊怎么让 AI 帮你把校验写对。我总结了几个提示词技巧,实测有效。
第一,明确要求校验维度。不要只说"加参数校验",要说"对每个字段做类型、长度、格式、范围校验,字符串字段设最大长度,数组字段设最大元素个数"。你要求得越具体,AI 生成得越完整。
第二,指定校验库。告诉 AI "使用 Pydantic 定义请求模型"或者"使用 Zod 做 schema 校验",它会生成更规范的代码,而不是手写一堆 if-else。
第三,要求处理异常路径。明确说"校验失败时返回统一的错误响应,不暴露字段细节,同时记录日志"。AI 默认不会考虑这些,你说了它才会做。
第四,让 AI 做对抗性审查。代码生成完之后,你可以再问 AI:"假设你是攻击者,会怎么构造输入来绕过这些校验?"它往往能给出一些你没想到的角度。这个技巧我用了很多次,效果不错。
4.5 第五步:把校验纳入 CI 流程
校验代码写完了,怎么保证后续不被改坏?答案是纳入 CI。具体做法有两个:一是写单元测试,覆盖各种非法输入;二是用静态检查工具扫描代码,发现没有校验的入口就报警。
单元测试这块,我建议专门写一组"恶意输入测试",把常见的攻击载荷都覆盖进去。比如超长字符串、负数、特殊字符、嵌套对象、类型混淆的值。这些测试用例可以复用,新接口直接套用。
import pytest from pydantic import ValidationError def test_reject_oversized_nickname(): with pytest.raises(ValidationError): UserProfileUpdate(nickname="a" * 1000) def test_reject_wrong_type(): with pytest.raises(ValidationError): UserProfileUpdate(nickname=12345) def test_reject_invalid_email(): with pytest.raises(ValidationError): UserProfileUpdate(nickname="ok", email="not-an-email")静态检查这块,可以写一个简单的脚本,扫描所有路由定义,检查是否绑定了校验模型。这个脚本不需要很复杂,能发现"裸奔"的接口就行。
5. 常见问题与排查技巧实录
5.1 校验规则太严导致正常请求被拒
这是最常见的问题。上线校验之后,发现部分正常用户请求被拒了。原因通常是校验规则定得太紧,没有考虑真实数据的多样性。比如昵称限制只能字母数字,结果用户用中文昵称就被拒了;比如备注限制 100 字符,结果用户粘贴了一段 150 字符的地址就被拒了。
排查思路:先看日志里被拒的请求,分析它们的共同特征,判断是攻击还是正常用户。如果是正常用户,就要放宽规则。我的经验是:校验规则上线前,先用真实数据跑一遍。把生产环境的请求日志脱敏后拿来测试,看看有多少会被新规则拒掉。如果比例超过 1%,就要重新审视规则。
5.2 校验通过但业务层仍然出错
有时候校验明明通过了,业务层还是报错。这种情况通常是校验和业务逻辑之间有"缝隙"。比如校验时字段是可选的,业务层却假设它一定存在;或者校验时字段是字符串,业务层却当数字用。
排查思路:检查校验模型和业务代码之间的契约是否一致。我建议在校验通过之后、进入业务逻辑之前,加一层"断言",把业务层依赖的前提条件再确认一遍。虽然有点冗余,但能快速定位问题。
5.3 性能问题:校验拖慢了接口
校验本身是有开销的,尤其是复杂的正则和深层嵌套的校验。如果发现加了校验之后接口变慢,先别急着砍校验,而是分析开销在哪。
常见的性能问题有两个:一是正则回溯,前面提过;二是重复校验,同一个数据在多个层被反复校验。解决办法:正则用非回溯引擎或加超时;校验只做一次,在边界处做完之后,内部传递时不再重复校验。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决思路 |
|---|---|---|---|
| 正常请求被拒 | 规则过严 | 分析被拒请求特征 | 放宽规则,用真实数据回归 |
| 校验通过但业务报错 | 契约不一致 | 检查校验与业务假设 | 加断言,统一契约 |
| 接口变慢 | 正则回溯或重复校验 | 性能分析定位热点 | 换正则引擎,去重校验 |
| 漏校验某入口 | 入口梳理不全 | 重新梳理所有入口 | 补校验,加静态检查 |
| 错误信息泄露内部结构 | 异常处理不当 | 检查错误响应内容 | 统一错误响应,细节进日志 |
5.5 几个我踩过的坑
第一个坑:以为框架会自动校验。有些框架确实会自动校验,但只校验类型,不校验长度和格式。我曾经以为 FastAPI 会帮我校验字符串长度,结果发现它只校验类型,长度要自己在 Field 里声明。这个坑让我在一个接口上漏了长度校验,好在测试阶段发现了。
第二个坑:校验规则写在了错误的位置。有一次我把校验写在了业务函数里,结果另一个入口调用同一个业务函数时绕过了校验。后来改成在边界处校验,问题解决。这个教训让我明白:校验必须在数据进入系统的第一道关口做,不能依赖业务层。
第三个坑:忽略了数组元素的校验。前面提过,只校验了数组长度,没校验元素内容。攻击者在数组里塞了一个超长字符串,绕过了长度校验。后来改成对每个元素都校验,问题解决。
第四个坑:AI 生成的正则有隐藏问题。有一次让 AI 生成一个 URL 校验正则,测试时发现它能接受javascript:alert(1)这种危险协议。后来换成成熟库,问题解决。这个坑让我对 AI 生成的正则始终保持警惕,一定要做对抗性测试。
6. 把校验变成团队习惯,而不是一次性任务
聊了这么多技术细节,最后我想说一个更根本的问题:边界校验不是写完就完事的任务,而是需要长期坚持的习惯。我见过太多团队,安全事件发生之后突击加一波校验,过几个月新接口又开始裸奔。这种"运动式"的安全治理,效果非常有限。
要让校验成为习惯,我觉得有三件事必须做。第一,把校验模板固化下来。团队里维护一套标准的校验模型模板,新接口直接复制修改,而不是从零写。第二,把校验纳入代码审查清单。每次 code review 都检查新接口有没有校验,没有就打回。第三,定期做对抗性测试。每隔一段时间,让团队成员扮演攻击者,尝试绕过现有校验,发现漏洞就补。
用 AI 写代码这件事,本身不改变安全的基本原则,但它改变了"谁来写校验"这个问题。以前是程序员写,程序员知道要校验;现在是 AI 写,AI 不知道要校验,需要程序员在提示词里明确要求。所以用 AI 的时代,程序员的安全意识反而更重要了,因为你要替 AI 把好这道关。
我个人的做法是:每次让 AI 生成接口代码,都会在提示词最后加一句"对所有外部输入做严格的类型、长度、格式、范围校验,使用 Pydantic 定义请求模型,校验失败返回统一错误响应"。这一句话能让 AI 生成的代码质量提升一个档次。你也可以试试,把它加进你的提示词模板里。
校验这件事,做的时候觉得繁琐,出事的时候才知道值。边界处多写几行校验代码,比事后救火划算得多。