
从一次很常见的开发任务说起用户中心需要提供一个订单查询接口开发同学把需求发给 AI 辅助编程工具不到一分钟AI 就生成了一套“结构完整”的接口代码。接口看起来有统一返回体注释齐全甚至考虑到了参数校验。可发布到测试环境后安全测试随手改了 URL 里的订单号就直接查到了另一个用户的订单。这就是一次典型的越权漏洞。这类问题并不是个例。AI 辅助生成代码早已不新鲜但“AI 产出的代码是否理解权限边界”这件事并不会因为模型变强就自动解决。更值得反思的是在被越权漏洞上了一课之后我又尝试用 AI 协助写一篇技术复盘文章结果 AI 输出的复盘内容暴露出另一个问题它把推测写成事实把模糊的排查过程写成确定性结论看起来逻辑完整实际上经不起逐句核对。这篇文章想聊两件事。第一AI 辅助开发时为什么容易出现越权漏洞以及如何用最小工程手段在 AI 生成的代码里补齐权限校验。第二当我们用 AI 做知识输出、技术复盘时它的局限到底在哪里怎样让它从“看着像那么回事”变成“真的经得起验证”。如果你平时也在用 AI 写代码、做技术总结这篇文章值得读完。1. AI 生成的代码为什么容易“越权”越权漏洞在安全领域非常常见它指的是一个已登录用户可以访问、修改、删除另一个用户的数据。很多团队第一次接触越权往往不是从安全文档里学到的而是从一次线上事故中被迫接受的。我见过不少团队把需求提交给 AI 编程助手AI 能在几秒钟内生成一套完整的增删改查接口。开发同学拿到后简单改改就能跑通。但问题恰恰隐藏在这里AI 生成代码时本质上是在做“模式补全”。它从海量训练数据里学会了“查询订单接口应该怎么写”但它看不到你项目里的登录会话、用户身份、权限模型。它并不知道一个查询订单的接口后面还应该有一句“当前用户是否是这个订单的主人”的判断。换句话说AI 优化的是代码产生的速度而不是业务权限的完整度。它默认假设调用方是可信的默认认为传入的参数就是合法的——这两个默认恰好是越权漏洞的土壤。1.1 水平越权与垂直越权我们先明确概念。越权漏洞通常分为两类。类型含义典型场景危害水平越权同级用户之间互相访问数据普通用户 A 查询到普通用户 B 的订单个人隐私泄露、数据篡改、账户信息被遍历垂直越权低权限用户访问高权限功能普通用户调用管理员接口后台被篡改、配置被修改、系统被接管在 AI 生成的业务代码里水平越权最容易被忽略因为 AI 只看到接口层面的参数看不到调用者的身份。而垂直越权往往发生在另一个场景AI 根据“这个功能需要有管理端操作”的描述生成了接口但接口上没有管理员角色校验普通用户照样能调用。1.2 为什么 AI 辅助开发更容易踩坑很多人以为越权漏洞是新手才会犯的初级错误。但现实是越权问题在 AI 辅助开发中反而更容易出现原因有四条。第一AI 生成代码时缺乏业务上下文。它不知道订单归属于哪个用户不知道部门维度、租户维度是否隔离更不知道组织架构里存在什么继承关系。第二AI 倾向于“最小实现”。用户问“帮我写一个查询订单的接口”AI 就会尽量简洁地返回查询逻辑不会主动多加一层鉴权判断。生成代码越短看起来越清晰越容易被接受。第三代码评审的注意力被转移。当一段代码是 AI 生成的开发同学会把精力放在“能不能跑通”上而不是“是不是安全”上。人会对机器生成的代码产生一种盲区仿佛 AI 已经帮我们把所有问题都想好了。第四越权漏洞本身是“静默的”。它不需要特殊权限不需要加密破解只需要正常登录后修改一个 ID 参数。程序能正常运行业务能正常返回安全测试不专门去测根本不会暴露。一条关键的判断是AI 辅助开发真正降低了“写代码”的成本但并没有降低“理解业务边界”的成本。越权风险不是被 AI 消除的而是被 AI 的“流畅输出”掩盖了。2. 一个最小可复现的越权接口为了把问题讲清楚我们用一个非常常见的 Spring Boot 场景来复现。假设有一个订单系统订单表里包含用户 ID、订单金额、订单状态用户可以通过订单号查询自己的订单详情。2.1 基础数据模型-- 文件路径src/main/resources/db/schema.sql CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10, 2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY idx_order_no (order_no) );这个数据模型本身没有安全问题。真正的问题会出现在查询接口的实现上。2.2 AI 容易生成的“问题版本”如果用一段很简单的提示词让 AI 生成查询接口很容易得到下面这种实现。// 文件路径src/main/java/com/example/demo/controller/OrderController.java RestController RequestMapping(/api/order) public class OrderController { GetMapping(/detail) public ResultOrder getOrderDetail(RequestParam(orderNo) String orderNo) { Order order orderService.getByOrderNo(orderNo); return Result.success(order); } }// 文件路径src/main/java/com/example/demo/service/OrderService.java Service public class OrderService { Autowired private OrderMapper orderMapper; public Order getByOrderNo(String orderNo) { return orderMapper.selectByOrderNo(orderNo); } }这段代码能跑通而且逻辑极其清晰根据订单号查库返回结果。但它没有做任何“这个订单是否属于当前用户”的判断。攻击者的操作方式非常简单正常登录自己的账号拿到自己的一个订单号比如202501010001将请求参数改成202501010002接口返回了另一个用户的订单详情。整个过程不需要任何特殊工具浏览器开发者工具就能完成。这就是典型的水平越权。2.3 问题版本的 SQL 与数据层实现数据层如果使用 MyBatisMapper 可能长这样。// 文件路径src/main/java/com/example/demo/mapper/OrderMapper.java public interface OrderMapper { Select(SELECT * FROM t_order WHERE order_no #{orderNo}) Order selectByOrderNo(String orderNo); }问题非常清晰SQL 里只有order_no条件没有user_id条件。数据库层面根本不限制查询归属那应用层就必须自己承担这个责任。AI 生成的代码恰恰忽略了这个责任。2.4 如何修复应用层增加归属校验修复方式不复杂核心原则是任何对业务数据的访问都必须同时携带“访问者身份”和“资源归属”并且在查询后做一次匹配校验。// 文件路径src/main/java/com/example/demo/controller/OrderController.java RestController RequestMapping(/api/order) public class OrderController { GetMapping(/detail) public ResultOrder getOrderDetail(RequestParam(orderNo) String orderNo) { Long currentUserId UserContext.getCurrentUserId(); Order order orderService.getOwnedOrder(orderNo, currentUserId); return Result.success(order); } }// 文件路径src/main/java/com/example/demo/service/OrderService.java Service public class OrderService { Autowired private OrderMapper orderMapper; public Order getOwnedOrder(String orderNo, Long userId) { Order order orderMapper.selectByOrderNoAndUserId(orderNo, userId); if (order null) { throw new BusinessException(CommonErrorCode.RESOURCE_NOT_FOUND); } return order; } }// 文件路径src/main/java/com/example/demo/mapper/OrderMapper.java public interface OrderMapper { Select(SELECT * FROM t_order WHERE order_no #{orderNo} AND user_id #{userId}) Order selectByOrderNoAndUserId(String orderNo, Long userId); }这里有三个关键改动。第一查询方法从selectByOrderNo改成了selectByOrderNoAndUserIdSQL 条件里强制带上了user_id。即使有人修改了订单号也只会查出属于自己的订单。第二当查询结果为空时统一抛出“资源不存在”的异常而不是“订单不存在”和“无权访问”分开报错。分开报错会泄露订单是否存在给攻击者提供枚举线索。第三在查询前通过UserContext获取当前登录用户 ID。这个 ID 必须由服务端会话解析出来绝不能从请求参数里读取。修复之后的代码即使 AI 再去生成类似接口只要把“归属校验”作为硬性要求写进提示词输出质量也会明显提升。3. 让 AI 生成的接口不再越权的工程化手段修复一个接口容易难的是让整个团队的代码都具备同样的安全意识。尤其在使用 AI 辅助开发之后代码产出的速度大幅提升如果安全防线还停留在“靠人提醒”的阶段漏洞数量会随着代码量一起增长。3.1 在提示词中显式声明安全要求很多开发者用 AI 写代码时提示词只写了“帮我写一个查询订单接口”这等于没有提供安全上下文。更好的做法是把权限规则写入提示词。请帮我用 Spring Boot 实现一个订单查询接口。要求如下 1. 接口路径为 GET /api/order/detail参数为 orderNo 2. 必须在查询前从 UserContext 中获取当前登录用户 ID 3. 查询时必须同时使用 orderNo 和 userId 作为条件 4. 查询结果为空时返回统一错误码 RESOURCE_NOT_FOUND 5. 不允许通过 request 参数传递 userId。同样的逻辑如果把“当前用户 ID”当成普通参数传给 AIAI 很容易写出从请求里读取userId的代码那又会产生新的越权。提示词是 AI 辅助开发的第一道约束。3.2 建立统一鉴权组件与其在每个接口里手写归属校验不如建立一套统一的鉴权模型。常见的方案是引入基于注解的权限校验。比如自定义一个OwnedResource注解结合切面自动完成“资源归属校验”。// 文件路径src/main/java/com/example/demo/security/OwnedResource.java Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface OwnedResource { String resourceType(); String resourceIdParam(); }这个注解不是万能的但对于水平越权高发的“主数据查询”场景能把校验逻辑集中起来避免每个接口都重复写一遍。AI 生成的新接口只要约定必须加这个注解漏判概率就会下降。3.3 代码审查清单团队可以约定一份“AI 代码安全审查清单”每次合并代码前逐项确认。检查项是否通过接口是否从请求参数中读取用户 ID否查询 SQL 是否携带 user_id 或租户隔离条件是是否对资源归属做了查询后校验是异常信息是否存在数据枚举风险否接口是否处于管理员角色保护下是这份清单可以快速排查 AI 代码中最常见的四类问题缺鉴权、缺归属校验、参数校验不严、异常信息过度暴露。3.4 接入静态扫描与自动化安全测试在工程层面建议把越权漏洞的检测自动化。静态代码扫描工具可以配置规则比如禁止在 SQL 中使用“单条件下的归属查询”。更有效的是在接口测试阶段加入越权用例。# 示例使用 curl 模拟水平越权检测 # 1. 使用用户 A 的登录凭证 curl -i -H Authorization: Bearer USER_A_TOKEN \ http://localhost:8080/api/order/detail?orderNo202501010002 # 2. 对照响应码判断是否返回了用户 B 的数据如果接口返回了正常数据说明存在水平越权。这条命令应当被写进自动化测试脚本每次构建后自动执行。它能覆盖人工容易遗忘的场景也能覆盖 AI 代码生成的“盲区”。4. 用 AI 写复盘文章为什么“又被上了一课”先回到标题里的第二件事。越权漏洞修复之后正常的流程是写一篇技术复盘记录问题原因、修复方案和预防措施。为了提高效率我尝试用 AI 辅助输出这篇复盘文章结果又踩了几个意想不到的坑。第一个坑是“AI 填充了不存在的细节”。让 AI 整理这次越权复盘时它会自动补出“经过 3 小时的代码审查”“与安全团队沟通了 2 轮”“问题在 2025 年 1 月 3 日正式确认”这样的细节。这些细节不是事实而是 AI 为了生成流畅文本而构造的“合理内容”。如果在复盘文章里不加以核对就会把虚构内容当成真实记录。第二个坑是“AI 把推测写成了结论”。真正的复盘需要区分“事实”和“假设”。比如“开发者在编写接口时没有考虑数据归属”是事实“开发者对权限模型理解不足”是猜测。AI 分不清这两者。它会用同样肯定的语气写出“因为开发团队对权限模型理解不足导致漏洞产生”听起来顺理成章但完全没有证据支撑。第三个坑是“AI 的复盘模板化”。大量训练数据让 AI 学会了标准复盘的结构比如“背景、问题、原因分析、解决方案、改进计划”。结构没错但内容被填充成“我们深刻认识到安全的重要性”“今后要加强代码审查”这类空话。这类复盘文章发出去读者看不到任何增量信息。第四个坑是“AI 不具备真实的决策链记忆”。复盘最有价值的部分是还原当时为什么选择 A 方案而不是 B 方案。AI 不知道当初的讨论过程不知道业务压力和排期约束也不知道开发者在深夜排查时的真实状态。但它会把这些信息编造出来让你误以为复盘已经足够完整。这四个坑可以用一个更本质的问题来概括AI 产出的是“看起来合理的文本”而不是“基于真实经验的记录”。文本的流畅度和可信度是两个维度。AI 在第一个维度上远远超出人类但在第二个维度上依然依赖人的输入和审核。4.1 一个典型的 AI 复盘输出示例为了看得更清楚可以模拟一次 AI 复盘输出。【问题背景】 订单查询接口存在水平越权漏洞普通用户通过修改订单号即可查询他人订单信息。 【原因分析】 经过详细排查发现开发团队在实现接口时缺乏权限校验同时对业务权限模型理解不足未充分考虑数据归属问题。 【修复方案】 1. 在查询 SQL 中增加 user_id 条件。 2. 增加统一异常处理避免资源枚举。 【改进计划】 后续将加强安全培训建立代码审查规范引入自动化安全扫描工具。这段内容有没有问题单看结构好像写得还行。但逐句分析“经过详细排查”——排查过程是什么排查了几步没有写。“开发团队对业务权限模型理解不足”——这是结论不是事实。凭什么判断是理解不足而不是漏写两者是完全不同的原因。“后续将加强安全培训”——这是套话没有可执行的下一步。真正的修复过程中涉及的具体文件和关键代码一个都没有出现。更好的复盘应该包含问题在哪一行被引入、触发条件是什么、测试用例是什么、修复 commit 是什么、有哪些地方还需要后续跟进。4.2 为什么 AI 在写“复盘”这件事上天然受限原因在于复盘是一种高度依赖事实和上下文的文体。AI 训练语料里有大量复盘文章但它无法访问你的 Git 提交记录、你的接口测试报告、你的数据库日志。它只能根据你的只言片语利用语言模型对“复盘文章应该长什么样”的理解去生成。这就要求使用者必须改变输入方式。如果你只是丢给 AI 一句“帮我写这篇复盘文章”它只能给你一个符合“标准复盘”模版的空壳。如果你把真实的提交记录、异常堆栈、修复代码、测试命令都喂给它要求它只能基于这些材料输出AI 才能生成有价值的初稿。4.3 更有效的 AI 复盘提示词我调整了提示词策略核心是“让 AI 基于素材生成而不是基于想象生成”。你是一名技术复盘写作助手。下面我会提供本次越权问题的真实材料。 请严格按照材料输出不得补充材料中不存在的事实。 材料列表 1. 问题接口OrderController.getOrderDetail 2. 触发条件修改 orderNo 参数即可查询非本人订单 3. 修复方式SQL 增加 user_id 条件查询后增加归属校验 4. 修复文件OrderController.java、OrderService.java、OrderMapper.java 输出要求 1. 先列出“已确认事实” 2. 再单独列出“待确认或推测内容” 3. 在推测内容前明确标注“推测” 4. 不要使用“经过详细排查”“深刻认识到”等空话 5. 输出格式背景、事实、修复动作、待办事项。这样的提示词有两个明显变化。第一AI 不再被要求“写一篇文章”而是被要求“整理已有材料”幻觉空间被压缩。第二输出中明确分离了“事实”和“推测”读者一眼就能看出哪些内容可以直接采信哪些需要继续确认。5. 从两次“翻车”里沉淀的工程判断一次是 AI 生成的代码出了越权漏洞一次是 AI 生成的复盘文章出现了虚构细节。表面上看一个是开发问题一个是写作问题但底层是同一个逻辑AI 擅长生成流畅的、符合模式的内容但无法自动理解你的业务边界和真实性需求。5.1 代码场景把 AI 当“生成器”而不是“决策者”在代码场景里正确的使用方式是让 AI 负责生成草案由人来负责安全设计和边界判断。责任也不会因为代码是 AI 写的就消失。开发流程可以参考这样的顺序人工先定义权限模型数据属于谁谁能访问管理员能做什么。把权限模型写进提示词让 AI 在生成代码时默认遵守。AI 生成代码后人工执行一次“越权视角”审查专门检查资源归属和角色校验。补齐自动化安全测试用例让越权检测进入 CI 流程。这四步中真正关键的是第一步。如果连“订单属于用户”这个基本关系都没定义清楚AI 生成再多的代码也只是在错误的地基上盖楼。5.2 文章场景把 AI 当“结构整理器”而不是“事实创造器”在写复盘文章和技术总结时同样的原则也适用。AI 可以帮你做结构整理、语法润色、要点提炼但它不应该负责“创造”你经历过的故障时间、Bug 现象和修复过程。更稳妥的做法是先人工列出这次事故的时间线和事实清单把清单交给 AI让它生成结构化的初稿人工逐句核对初稿删除 AI 自行补充的模糊细节在成稿里明确区分“事实”“推测”“待确认”三类内容。这套流程看似保守却能保证文章的可信度。技术文章最大的资产是可信度。一篇充满幻觉细节的文章即使结构流畅也会让读者失去信任。5.3 关于 AI 幻觉的再认识AI 幻觉不是偶尔出现的 bug而是语言模型在生成文本时的一种内在倾向。模型追求的是“最可能连续的文本”而不是“与外部世界一致的事实”。只要使用 AI 生成涉及具体事实的内容就必须设计校验机制。在代码领域校验机制是编译、测试、安全扫描在文章领域校验机制是人工核对、材料引用、事实标注。两种机制的共同点是它们把最终判断权保留在人手里。6. 实用检查清单AI 辅助开发与写作的避坑指南把上面所有的经验汇总成一份可以直接拿来用的清单。这份清单既适用于 AI 辅助写代码也适用于 AI 辅助写技术文章。6.1 代码场景避坑清单1. 是否在提示词中定义了用户身份如何获取 2. 是否在提示词中指定了资源归属字段 3. 生成的代码是否包含归属条件 4. 是否包含角色权限校验 5. 有没有从请求参数中读取 userId 的情况 6. 查询结果为空时是否返回统一错误码 7. 是否补充了越权自动化测试用例 8. 是否由人做了最终代码审查重点提醒最后一条无论 AI 生成的质量多高代码必须由真人审查并合并。AI 可以替你写代码但不能替你对代码负责。6.2 文章场景避坑清单1. 是否向 AI 提供了真实材料 2. 提示词是否禁止 AI 补充材料外的事实 3. 输出中是否区分了事实与推测 4. 是否逐句核对了所有时间、数字、人名、文件名 5. 有没有删除“深刻认识到”“进一步加强”等空话 6. 是否保留了具体的修复代码或命令 7. 是否删除了 AI 生成的“样板总结” 8. 最终发布前是否由熟悉本次事故的人复核过按照这份清单操作AI 产出的文章会更接近“可发布”而不是“看着能发布”。6.3 一个简便的 AI 内容校验方法如果你不确定 AI 生成的段落是不是幻觉有一个很简单的办法把这段内容中的每一个具体说法单独拎出来问自己“这个信息我有没有原始依据”没有依据的就是需要删掉或者标记为推测的。“修改了 3 个文件”——有没有 git diff 截图“排查耗时 2 小时”——有没有时间记录“与安全团队沟通了 2 轮”——有没有会议记录“漏洞在 v1.3.2 版本引入”——有没有版本发布记录技术写作不必追求每个细节都有文档但关键事实必须可追溯。AI 不会主动告诉你哪些内容没有依据所以你必须自己问。7. 总结AI 不会替你背锅回到标题里那两段经历。AI 越权“产卡”本质是 AI 生成的代码没有自动理解业务权限边界复盘文章被“上了一课”本质是 AI 生成的文本没有自动区分事实与推测。这两件事放在一起看结论非常清晰AI 能大幅提高产出速度但无法替代人对边界和真实性的判断。以后再用 AI 辅助开发和技术写作我会坚持三个原则。第一AI 产出的代码必须经过“越权视角”的专项审查。第二AI 产出的文章必须基于真实材料而不是基于它的模板记忆。第三提示词里明确要求 AI 标注“推测”和“不确定项”把事实核查的主动权牢牢握在自己手里。技术工具永远在变化但这些原则不会过时。也许下一次 AI 模型更强大能自动理解更多业务上下文但在那之前代码是你签的文章是你署名的责任始终在人的这一侧。