AI生成代码引发越权漏洞?从接口安全到复盘写作的实战教训
前段时间做内部系统的一个任务卡片模块,我让 AI 辅助写接口,速度确实快,接口也能跑通,但测试同学很快就测出一个典型安全问题:普通用户只要把自己的 cardId 换成别人的,就能读到不属于自己的卡片数据,也就是越权。更让我没想到的是,等我想把这次事故写成复盘文章时,又让 AI 帮我写,结果它给我吐出一堆“加强安全意识、完善权限体系”之类的空话,内容看起来流畅,实际上没有任何可复现信息。
这两件事连在一起,比单一事故更有启发。AI 越权产卡,是我对 AI 生成代码的安全边界认知不够;复盘文章翻车,是我对 AI 写作的定位选错了。下面我把这两次教训拆开讲,先复盘越权问题是怎么发生的,再聊 AI 辅助写复盘文章时该怎么做,最后给一份可以直接复用的事故复盘模板和排查清单。
1. 复盘一次典型的 AI 越权产卡事故
1.1 事故现象:换了别人的 id,就能查到别人的卡片
那个模块很典型,就是一张“任务卡片表”,字段包括卡片标题、内容、创建人、更新时间,前端需要展示当前登录用户创建的卡片列表,也要支持查看单张卡片详情。
当时我让 AI 生成后端接口,需求描述大概是“实现卡片列表查询和详情查询”,AI 很快给出了一份代码,核心逻辑看起来没啥问题。详情接口大概是这样的:
从功能角度讲,这个接口是能用的。登录用户请求 /api/cards/1024,就返回 id 为 1024 的卡片数据。问题在于,它只校验了“你是否登录”,完全没有校验“这张卡片是不是你的”。
换句话说,只要用户知道或者猜到一个 cardId,比如 1、100、1000,就可以不断遍历接口,把其他用户的卡片内容全部拉出来。这是典型的水平越权:两个同级别用户之间,能访问本不该访问的数据。
1.2 根因:功能需求写清了,权限约束没写进需求
看到这里,很多人第一反应是“AI 写代码不靠谱”。但更准确的归因是:我给 AI 的需求只覆盖了功能,没有覆盖权限约束。
“查询卡片详情”这个需求,至少包含两层含义:
- 功能层:根据 cardId 返回卡片数据。
- 权限层:当前登录用户只能查询自己创建的卡片。
AI 在生成代码时,会优先满足它能看到的最明确指令。如果 prompt 里只写了“按 cardId 查询”,它就自然生成一个按 cardId 查询的方法。至于 userId 从哪里来、要不要拼接进 SQL、查询不到时返回 404 还是 403,它不会主动替你设计。
还有一种更容易踩坑的情况:AI 可能主动生成类似这样的代码:
这看起来是做了隔离,实际上反而更危险,因为 userId 来自前端参数。攻击者把 userId 改成别人的,一样能查到别人数据。正确的做法是,当前登录用户的 ID 必须从服务端会话、Spring Security 上下文或统一的登录态工具里获取,绝不能信任前端传值。
1.3 修复思路:先改数据范围,再补统一校验
修复越权一定要先看清楚数据链路。我当时的修复可以浓缩成一句话:把“按 cardId 查”改成“按 cardId 和 userId 一起查”。
这里有几个细节值得注意。
第一,查询语句要同时带上 ownerId 条件,而不是先在内存里把数据查出来再判断。只要数据源是关系型数据库,就应该让 SQL 去过滤。
第二,查询不到时不要直接返回“没有这张卡片”,也不要把两条业务场景混在一起。遇到“卡片不存在”和“卡片不属于当前用户”时,为了不泄露资源是否存在,通常统一返回 404。
第三,详情接口改完,列表、更新、删除接口也要一起查。很多越权事故是修了详情,忘了更新接口也存在同样问题。
修复之后的验证标准很简单:用用户 A 的登录态去访问用户 B 创建的卡片 id,接口必须返回 404 或 403;用用户 A 自己的卡片 id,接口正常返回;列表接口只能返回当前用户的数据。
注意:修复越权不能只靠拍脑袋改一个接口。先梳理这个模块所有入口,再统一排查“当前用户从哪里来、数据 owner 怎么比对、查询条件有没有漏”。
2. AI 生成代码漏掉权限校验的四个真实原因
2.1 Prompt 里缺少安全约束,AI 默认只补功能
大多数 AI 辅助开发的场景,问题不在模型能力,而在任务定义。
如果 prompt 是“实现一个查询卡片详情的接口”,AI 会默认这是一段可运行的 CRUD 代码。它不会问“当前系统有没有 Spring Security”“用户 ID 放到哪里”“有没有角色权限”,因为这些信息你没给它。它只能根据公开代码里的常见写法去猜。
想减少这类问题,最简单的办法是:把安全约束直接写进 prompt 开头。比如:
这个 prompt 不是万能,但至少把最关键的权限边界讲清楚了。AI 再生成代码时,跑偏的概率会明显下降。
2.2 AI 训练数据里大量 CRUD 模板本身就缺少鉴权
这个原因很容易被忽略。模型学习时会见到大量公开代码,而公开代码里有相当大比例是教学 Demo、内网工具、个人项目片段。这些代码为了突出核心业务逻辑,往往会把 Spring Security、Shiro、Session 等上下文省略掉。
于是 AI 产生了一种“特征关联”:看到 @GetMapping 加 findById,就认为这样是完整的。它不是在故意写漏洞,而是在高概率复现它见过的“常见写法”。
理解了这一点,就不该把 AI 当成有安全能力的高级工程师,而是当成一个语法熟练、但经常缺少工程上下文的“代码生成器”。生成之后的安全审查,必须由人来补。
2.3 上下文太短,AI 看不到工程里的权限体系
有时你只把单个 Controller 文件贴给 AI,它确实看不出哪里有越权风险。因为权限信息可能写在 Filter、拦截器、SecurityConfig、Service 层。如果这些上下文没给到 AI,它连项目里有没有统一鉴权都不知道。
更合理的做法是,把权限相关的关键代码一并放进 prompt。比如:
- 当前用户怎么获取:是
@AuthenticationPrincipal,还是SecurityContextHolder。 - 是否已经有权限注解:
@PreAuthorize、@RequiresPermissions。 - 数据访问层有没有现成的
findByIdAndUserId方法。
AI 看到这些上下文后,判断会准确很多。即使它仍然没有写出最终代码,也可以给出更接近项目实际的建议。
2.4 功能测试通过,不代表权限测试通过
很多项目自测只关心一件事:登录之后能不能正常访问到数据。这个流程能跑通,就觉得功能完成了。但越权问题恰好藏在这个流程之外。
越权测试的核心是交叉验证:用户 A 创建数据,用户 B 访问 A 的数据 id,看接口返回什么。如果接口照样返回数据,那就有问题。
这类用例非常简单,但经常被忽略。我建议在关键接口的测试代码里至少保留两条:
- 正常用户访问自己数据,返回 200。
- 普通用户访问其他用户数据,返回 403 或 404。
只要这两条用例存在,AI 以后再怎么生成接口,只要跑一次测试就会发现回归。
3. 用 AI 辅助排错时,先走这条路
3.1 先复现,再谈修复
遇到越权报告,第一件事不是改代码,而是复现。
我当时会先准备两个测试账号,A 和 B。A 创建一张卡片,拿到 cardId。B 登录之后,用这个 cardId 请求详情接口。如果 B 拿到了 A 的卡片数据,越权就坐实了。
为了让复现过程更清晰,可以把请求信息整理成一条伪 curl 记录:
实际项目里 token 需要替换成真实值,这里只是示意。关键是记录三件事:请求地址、请求参数、当前登录身份。越权问题没有这三件套,很难定位。
AI 在复现阶段能帮你做什么?它可以帮你生成 curl 命令、写测试脚本,甚至根据报错日志推测问题方向。但它不会替你完成“用两个账号交叉访问”这个动作。复现必须由人在真实环境里跑。
3.2 用最小测试用例验证权限边界
修复越权时,不要一上来就覆盖所有功能,先用最小测试用例把权限边界定住。
最小测试用例可以写成三步:
- 用户 A 创建一个资源,得到资源 id。
- 用户 B 登录后访问该资源 id。
- 断言返回结果不是资源明文数据,而是 403 或 404。
这个用例不止用于手工测试,也应该能写进自动化测试。对 Java 项目来说,可以是 Spring Boot 的 MockMvc 测试;对前端项目,也可以是接口测试脚本。
一个值得反复强调的是:越权用例要尽量多覆盖几个操作。查询接口只是最低标准,更新、删除、列表接口都需要同样的交叉验证。否则就会出现“详情接口修好了,更新接口还能被别人改”的尴尬。
3.3 让 AI 当代码审查员,但别让它直接下结论
给 AI 贴代码时,你可以让它“从安全角度找问题”。但要注意,AI 给出的判断只能作为参考,不能当成最终结果。
原因是 AI 写代码时会漏上下文,审代码同样会漏。它可能把你给出的代码当成完整工程,忽略日志、拦截器、数据库权限等其他环节。它会漏报,也可能会误报。
更稳妥的用法是:把接口代码、当前用户获取方式、DAO 查询方法一起发给 AI,请它列风险点和测试建议。然后自己沿着这些点去核对代码。
我常用的一句话是:
请假设自己是代码审查员,只看我给出的代码,不要推测工程里不存在的过滤器。列出可能导致水平越权的点,并给出对应的测试用例。
这样 AI 的回复会更保守,也更接近工程实际。
4. 复盘文章输出翻车:AI 写作的问题并不在文笔
4.1 AI 写出来的复盘为什么又空又长
代码越权这个事故,处理完其实不算复杂,但我想写一篇团队内部复盘文章。为了省时间,我把“事故经过”和“根因分析”丢给 AI,让它生成一篇技术复盘。
结果它生成的内容大概是:
“本次事故暴露了系统在权限管理方面的不足,需要我们加强安全意识,完善权限体系,提升整体安全防护能力。”
这类句子看上去没问题,其实一点价值都没有。因为它不包含任何可复现的信息,读者不知道该改哪一行代码,也不知道怎么测试才能避免同类问题。
问题不在 AI 文笔不好,而在于我没有给它足够的事实。我只给了“越权”这个关键词,它就只能调用训练数据里的“越权模板”。文笔越流畅,反而越容易掩盖事实缺失。
4.2 把写作任务拆成素材整理、逻辑检查、结构优化三个环节
经历了这次翻车,我现在用 AI 写技术复盘,不再让它从零生成全文,而是拆成三个环节:
第一,先自己把事实素材整理出来。时间、接口、参数、代码、日志、修复前后差异,这些都是 AI 无法替你编造的。AI 一旦开始编造细节,文章就越看越假。
第二,让 AI 检查逻辑漏洞。你可以把已写好的事实清单发给 AI,请它判断“从现象到结论的因果关系是否完整”“有没有从某个现象直接跳到结论,缺少中间证据”。
第三,让 AI 做结构优化。比如补充小标题、调整段落顺序、建议代码块位置,这些都是 AI 比较擅长的局部优化工作。
这样分工之后,AI 不再负责“创造事实”,而是负责“整理和检查”,文章质量会稳定很多。
4.3 一份能复用的 AI 写作辅助提示词
如果你也想让 AI 帮自己写复盘,可以试试这个思路,而不是直接说“帮我写一篇复盘文章”:
这样写有几个好处。第一,AI 不会自作主张编造你没写过的细节。第二,它把重点放在检查和修改上,而不是从零生成。第三,你能保留文章最核心的“现场感”,AI 只负责把结构理顺。
注意:AI 写作最容易出问题的不是语言,而是事实。越具体的时间线、接口路径、代码片段,越要自己写进去,不要让 AI 猜。
5. 真正能落地的事故复盘模板和排查清单
5.1 越权类问题排查清单
如果你接下来要去排查一个疑似越权的问题,可以参考下面这个清单:
| 检查点 | 具体内容 | 判断标准 |
|---|---|---|
| 请求参数 | 接口是否从前端接收 userId、ownerId、角色ID等身份类参数 | 身份信息必须来自服务端会话或安全上下文 |
| 当前用户获取 | 查询当前登录用户的方式是否统一 | 所有接口应使用同一套登录态工具 |
| 数据查询条件 | SQL 或查询条件中是否包含数据归属过滤 | 按数据 id 查询时,必须同时带归属条件 |
| 列表接口 | 是否有按条件查询全部数据的能力 | 列表接口必须自动加上当前用户条件 |
| 更新删除接口 | 更新、删除前是否校验数据归属 | 非本人数据不允许更新或删除 |
| 接口返回 | 数据不存在时是否统一返回 404 或 403 | 避免暴露资源是否存在 |
| 日志 | 接口日志是否能区分操作者、操作对象 | 排查时能还原现场 |
这个列表不是要背下来的,而是排查时逐个过一遍。尤其要注意列表接口,因为它一旦越权,泄露的数据量会比单条查询更严重。
5.2 复盘文章写作骨架
写完代码之后,复盘文章可以按这个骨架组织:
- 事故时间线:什么时间、哪个接口、出现什么现象。
- 影响范围:哪些用户可能受影响,哪些数据存在泄露风险。
- 根因分析:是哪一行代码、哪一个条件漏掉了。
- 复现步骤:用什么账号、访问什么地址、返回了什么结果。
- 修复方案:给出关键代码修改,说明改动前后的行为差异。
- 验证方式:哪些自动化测试用例、哪些手工回归步骤。
- 后续预防:prompt 约束、代码评审、权限测试用例是否需要补。
每个部分都要用具体内容填充。如果某个部分写不出来,说明你对事故的理解还不够完整,先去查代码和日志,而不是硬写。
5.3 根据实际场景调整边界
复盘模板不是死的。
如果这次事故涉及用户个人信息,那影响评估要写得更仔细,要说明是否包含敏感字段、是否需要通知相关人员。如果涉及批量导出接口,还要多检查数据遍历风险。如果项目里已经有一套 RBAC 权限体系,那要说明为什么 AI 生成的代码没有自动走这套体系。
我自己写复盘时,会刻意避免一个倾向:把所有问题都总结成“安全意识不足”。这句话太容易写了,但它不能指导下一次开发。更好的写法是:“这次事故是因为 prompt 里没有强调用户数据隔离,导致 AI 生成的查询语句只按 cardId 过滤。后续所有 AI 生成的数据接口,都要在 prompt 里写明归属校验要求。”
这样写,下一名开发看到后,至少知道改自己的 prompt,而不是空喊口号。
6. AI 辅助开发不是撒手不管,三件事值得坚持
6.1 把安全约束写进初始 Prompt
用过 Cursor、Copilot、JetBrains AI Assistant 这类 AI 编程工具的人,应该都有类似体验:AI 生成代码非常快,但如果你不给它边界,它就会按最普通的模板写。
因此,我现在让 AI 写数据接口时,会强制在 prompt 开头写清安全约束。一般会包含这些内容:
- 当前用户从什么地方获取,禁止从前端请求参数读取。
- 所有查询必须包含数据归属条件。
- 不需要输出整个工程,只需提供当前文件的关键代码。
- 生成完成后,单独列出测试用例。
这个习惯不复杂,但能显著减少越权、硬编码、敏感信息在日志中打印等问题。
6.2 用代码评审和自动化测试兜底
AI 生成代码之后,代码评审不能省。越权问题靠肉眼 review 可以发现问题,但很难保证下次不出现。更可靠的是把权限测试写进自动化测试,用用例卡住回归。
在搭建测试用例时,不只在接口层做。如果项目里有批量任务,还要看 AI 生成的代码是否在循环查询、批量更新时正确处理数据权限。这类问题在单条接口上不容易暴露,但放到批处理里很容易出现。
另外,安全测试和功能测试要分开。功能测试验证“能跑”,安全测试验证“不能越权”。AI 生成代码后的自测,必须同时包含这两种视角。
6.3 关注 AI 工具的边界和成本
现在很多 AI 编程工具都按 credits 计费,或者说按额度计费。生成代码、对话、分析代码都会消耗额度。只看“生成了多少行代码”来计算成本,很容易忽略真正的消耗在于评审、返工和测试。
我现在的习惯是:小任务直接用 AI 生成,中等任务让 AI 生成初稿,大任务先自己画好接口边界和鉴权逻辑,再让 AI 填充。越是核心、越涉及用户数据的接口,越不能全权交给 AI。
如果你对数据隐私要求高,或者想减少对云端 API 的依赖,也可以考虑本地部署模型。但本地部署不是免费的,显存、内存、推理速度、模型版本都会影响实际效果。低配置机器可以跑,但别指望本地小模型能像云端大模型一样几秒生成完整代码。
AI 工具本身没有好坏,关键是使用方式。把安全边界和事实素材准备好,再让 AI 去生成代码或写复盘,它才能真正变成生产力。这两次“上课”给我最大的收获不是更会写 prompt,而是养成了一个习惯:在把任务交给 AI 之前,先想清楚哪些东西必须由人来定,哪些东西可以放心让 AI 发挥。