尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

AI 生成代码越权翻车实录:普通用户能导出全量订单,我复盘了根因

AI 生成代码越权翻车实录:普通用户能导出全量订单,我复盘了根因 摘要一个“用户导出自己的订单”接口上线第二天就被安全测试用普通账号拉走全量订单。代码能查、能分页、能导出唯独缺了接口鉴权属于典型 IDOR 越权漏洞。根因是 AI 代码安全上下文里没有“当前用户是谁、订单归属谁”以为能查到就能看。本文复盘完整排查与修复对照 OWASP Top 10指出身份必须由服务端会话决定。文章目录上线第二天安全测试随手一点就穿了现象接口正常但没有边界排查不是查不到是根本没校验根因AI 不知道资源归属谁修复两个版本一个看似修了其实没修错误修复版从 query 挪到请求头以为换个地方就安全正确修复版身份必须来自服务端会话补一条回归测试这个洞AI 为什么特别容易埋常见越权接口清单除了导出订单这些接口也最容易被漏掉这套修复的边界总结一下上线第二天安全测试随手一点就穿了那个接口是导出订单给用户自己下载订单明细用的。需求不复杂——用户选时间范围导出自己名下的订单做成 Excel 或者 CSV。AI 写的代码。我看了一眼逻辑都对能按条件查、能分页、能拼 CSV、能设置文件名。功能测试也过了就发了。上线第二天安全测试的同事随手点了这个导出接口用另一个普通账号试了试结果导出了全量订单。不是当前账号的是所有人的。我当时第一反应是这不怪 AI 吧是我没让它做权限。但往下查了才发现事情没那么简单。现象接口正常但没有边界先说清楚这个接口的定位不然容易混淆——它设计上就是用户导出自己的订单不是后台管理那种导出全量数据的接口。所以它该做的是只允许当前用户导出自己名下的订单。AI 的实现大概是这样的去掉敏感信息后的简化版示例基于 Node 20 Express 5 Sequelize 6// 为什么要看这段AI 生成的导出接口看着能查能导出但没校验导出的是谁的订单app.get(/api/orders/export,async(req,res){const{userId,startDate,endDate}req.query// ← userId 从前端传进来constordersawaitOrder.findAll({where:{userId:userId,// ← 只按前端传的 userId 查createdAt:{between:[startDate,endDate]}}})res.setHeader(Content-Type,text/csv)res.setHeader(Content-Disposition,attachment; filenameorders.csv)res.send(buildCsv(orders))})// 翻车时的运行结果 正常调用/api/orders/export?userId1001startDate...endDate... → 查 userId1001 的订单正常导出自己那份 恶意调用/api/orders/export?userId1002startDate...endDate... → 照样查 userId1002 的订单并返回 ← 越权别人的数据被导出来了功能上它完全正常能查、能分页、能导出、文件名也对。问题在于——它只按前端传进来的 userId 查没有校验这个 userId 是不是当前登录的用户。我把 userId 改成别人的接口就返回别人的订单。这就是典型的 IDOR越权访问漏洞对应 OWASP Top 102021里的 A01 失效的访问控制在高危漏洞里常年排第一。排查不是查不到是根本没校验排查其实很顺因为问题很直接。我没去翻日志、没去抓包——我看了一眼代码就明白了。但看一眼就明白这件事本身让我不舒服。我在想为什么我当时 review 没看出来功能测试为什么也没测出来功能测试测的是正常路径——登录一个用户导出自己的订单能导出就通过。没人去测用 A 的登录态去导 B 的订单。安全测试测的恰恰是这条反例路径所以一测就穿。这条越权路径画成时序图如下DSAD[数据库]S[服务端]A[攻击者]DSAD[数据库]S[服务端]A[攻击者]漏洞点1userId 直接来自前端 query可被任意篡改漏洞点2服务端未校验该 userId 是否等于当前登录用户结果攻击者成功越权导出他人订单GET /api/orders/export?userId1002startDate...endDate...SELECT 订单 WHERE userId 1002返回 userId1002 的订单数据返回 1002 的全量订单 CSV根因AI 不知道资源归属谁这个洞的根因值得单独说清楚。我一开始以为责任全在我没让 AI 做鉴权。但复盘下来更准确的说法是AI 生成的代码里根本没有当前用户是谁和订单归属谁这两个概念。看 AI 的代码就明白了——它把userId当成一个从前端传进来的查询参数跟startDate、endDate一样。AI 的上下文里只有这个接口接收哪些参数没有这些参数里哪个必须由服务端身份决定、不能由用户自己传。在 AI 的认知里“能查到数据就约等于该返回数据”。它不知道订单是分属不同用户的、不知道这个接口背后有谁导出谁的这种边界。这不是 AI 故意埋洞是它的上下文里压根没有这些信息。所以这个翻车的责任与其说在AI 没写鉴权不如说在没人做安全审查。我 review 时只看了逻辑对不对能查、能导出、能分页没看信任边界这个数据该归谁。这正好是我在另一篇里说的——审 AI 代码不能只看对不对还要看边界在哪。修复两个版本一个看似修了其实没修修复越权核心就是加归属校验——只允许用户导自己的订单。但这里有个非常容易踩的坑我差点就踩进去了。错误修复版从 query 挪到请求头以为换个地方就安全AI或者说第一次想省事的我给出的修复是这样——它意识到userId放在 URL 参数里不好于是把它挪到了请求头以为不在 URL 里了别人就改不了了// 错误修复版看似修了其实只是把 userId 从 query 挪到了 headerapp.get(/api/orders/export,async(req,res){// 以为放到 header 就安全其实 header 也是前端可控的constuserIdreq.headers[x-user-id]// ← 还是前端传的只是换了个地方const{startDate,endDate}req.queryconstordersawaitOrder.findAll({where:{userId:userId,// ← 依然用前端传的 userIdcreatedAt:{between:[startDate,endDate]}}})// ... 导出 orders})// 错误修复版运行结果 恶意调用Header: x-user-id: 1002 → 攻击者直接改请求头里的 x-user-id → 照样导出 1002 的订单 ← 还是没修只是从 query 换到了 header看出问题了吗请求头也是前端可以随便设的跟 query 参数一样不可信。从 URL 挪到 header等于把门从左边挪到右边门还是开着的。判断这个身份可不可信不是看它放在 URL 还是 header而是看它是不是由服务端自己认出来的。凡是从前端任何可控位置query / header / body拿到的 userId都是不可信的。正确修复版身份必须来自服务端会话正确的做法是当前用户是谁必须由服务端从登录会话 / token 里解析出来绝不能从请求体里拿。前端传的userId直接忽略永远用服务端认出的那个身份// 正确修复版userId 必须来自服务端会话忽略前端传的值app.get(/api/orders/export,async(req,res){// 身份从服务端会话/token 解析不是从前端请求体拿constcurrentUserIdreq.session.user.id// ← 服务端认出的身份constordersawaitOrder.findAll({where:{userId:currentUserId,// ← 永远用服务端身份忽略 query 里的 userIdcreatedAt:{between:[req.query.startDate,req.query.endDate]}}})res.setHeader(Content-Type,text/csv)res.send(buildCsv(orders))})// 正确修复版运行结果 恶意调用/api/orders/export?userId1002... → currentUserId 从会话解析 登录用户自己的 id比如 1001 → 忽略前端传的 userId1002只查 1001 的订单 → 返回 1001 的订单 ← 越权被拦下拿不到 1002 的数据修复前后的差异画成图就是这样的修复前 浏览器(随便填userId) ── 服务端 ── 查 userId用户填的 ── 返回任意人的数据 修复后 浏览器 ── 服务端(从会话解析出当前用户) ── 只查当前用户 ── 返回本人的数据 │ userId 由服务端决定忽略前端传的三个版本一对比差别就清楚了版本userId 来源攻击者能控制吗结果翻车版URL 参数?userId✅ 能越权拉全量错误修复版请求头x-user-id✅ 能只是换个位置还是越权正确修复版服务端会话❌ 不能只查自己的判断身份可不可信不是看它放在 URL 还是 header而是看它是不是由服务端自己认出来的。这一条我现在每审一个 AI 生成的接口都会过一遍之前就吃过没查的亏。补一条回归测试光改代码还不够得把这条反例路径固化进测试防止以后又被 AI 或者粗心的改动带回去// 为什么要看这段把越权反例路径固化成测试防止回归it(普通用户不能导出别人的订单,async(){constloginUserawaitloginAs(user_1001)constresawaitloginUser.get(/api/orders/export?userId1002)// 正确修复后返回的应该是自己(1001)的订单不是 1002 的constdatares.bodyexpect(data.every((row:any)row.userId1001)).toBe(true)})// 测试运行结果 普通用户导出 userId1002 的接口 → 返回的全是 userId1001 的数据服务端身份 → 断言通过 ✅如果哪天又被改回从请求体拿 userId这条测试会立刻红这条测试的价值是它把反例路径越权变成了常规回归而不只是靠人记得去测。这个洞AI 为什么特别容易埋复盘完我想把为什么 AI 特别容易埋这种洞说透。因为 AI 生成接口的思考路径是接口接收什么参数 → 就按参数去查。它把userId、startDate、endDate一视同仁都是查询条件。它没有哪些参数该由服务端身份决定这个认知——它的上下文里没有信任边界。人写代码时会带着这是登录用户的 id得从 session 拿这种默认认知。AI 没有它只会按 prompt 里给的信息和最常见写法来。所以审 AI 生成的接口身份边界这块要专门看凡是接收 userId / ownerId / 各种资源 id 的接口都要确认这些 id 是服务端解析出来的不是前端随便传的。这是我在安全审查那篇里说的身份边界这里算是一个完整的事故案例。常见越权接口清单除了导出订单这些接口也最容易被漏掉导出订单只是其中一个例子。实际审 AI 生成的接口时这些带“资源 id”的接口都是 IDOR 高发点可以对照着查接口类型高风险点归属校验要点查看用户详情GET /api/users/:id改:id看别人的手机号、邮箱、地址当前用户身份从会话取路径里的:id必须等于当前用户否则 403修改用户资料PUT /api/users/:id改:id篡改他人昵称、密码、绑定手机只认服务端会话身份密码、手机等敏感字段还要二次校验查看订单/账单详情GET /api/orders/:id改orderId读他人订单查出后校验order.ownerId currentUserId不能直接按 id 返回修改/取消订单PUT /api/orders/:id改orderId修改或取消他人订单先校验归属再执行更新/删除语句里同时带userId条件删除资源DELETE /api/files/:id改:id删掉他人文件、评论、地址删除前必须归属校验批量删除要逐条校验不能只校验第一条下载文件/附件GET /api/files/:id/download改fileId拉取合同、发票等敏感附件校验文件归属或授权列表不能只校验“已登录”后台管理接口GET /api/admin/users普通用户访问管理能力只做归属校验不够必须再加角色/权限校验RBAC跨租户数据GET /api/tenants/:tenantId/...改tenantId跨租户读数据所有查询强制带租户上下文并校验当前用户所属租户这些接口的共同点都一样路径或参数里出现了userId、orderId、fileId这类资源 id。AI 很容易把它们当成普通查询条件直接拼进查询或更新语句。审的时候只要看到资源 id第一反应就应该是这个 id 是服务端身份解析出来的吗资源归属校验做了吗这套修复的边界归属校验能挡住普通用户越权访问别人资源IDOR这一类但要说清楚它覆盖不到什么。不覆盖横向越权里更复杂的权限模型。比如多级角色普通用户 / 运营 / 管理员、租户隔离一个平台多个租户、部门数据权限。这些不是一个 userId 校验能解决的需要完整的权限模型RBAC / ABAC。不是所有接口都要归属校验。公开数据、不需要登录的接口强行加归属校验反而是错的。审查时要分清这个数据本来就该公开还是这个数据有归属。我整理了一张简单的判断表审接口时直接套接口类型要归属校验吗为什么用户导出自己的订单 / 报表✅数据分属不同用户用户查自己的资料 / 订单详情✅数据有归属后台管理导出全量✅且要管理员角色除了归属还要角色校验公开商品列表 / 公告❌本就公开跨租户共享数据✅租户隔离归属到租户级别只挡越权不挡别的洞。校验的是这个资源该不该给你看挡不了参数注入、业务逻辑漏洞这类别的问题。总结一下回头看这个翻车其实是个特别典型的 AI 代码安全坑代码功能全对但边界错了——它把用户身份当成了一个可以随便传的查询参数。根因不在 AI 故意埋洞而在没人做安全审查。我 review 的时候只看了逻辑对不对没看信任边界在哪。这个教训我后来沉淀成一句话也写进了主推那篇审 AI 生成的接口身份边界是第一个要看的——资源 id 必须来自服务端身份不能从前端任何可控的位置拿。这条过了越权这一类洞基本就堵住了。本文涉及的相关文章AI 生成代码安全审查实战三条信任边界主推本弹安全防线框架AI 代码 Review 实战从看代码对不对到看 AI 懂不懂AI 代码信任分级实战从全信到三档我在哪些代码上敢让 AI 自己跑
返回列表