
摘要AI 生成的代码逻辑全对、能跑、返回正常但安全测试一测就穿。原因在于安全漏洞不是写错了是 AI 不知道你没告诉它的信任边界——输入从哪来、谁能操作、数据往哪流。本文提出三条信任边界的审查框架输入边界、身份边界、数据边界每层给出AI 生成时的典型漏洞形态 审查方法 看似修了其实没修的反例文末附带可直接复用的安全检查清单和决策表。文章目录一个逻辑全对的接口安全测试一测就穿安全审查画的是信任边界第一层输入边界——数据从哪进来AI 生成时的典型形态动态查询条件拼进原生 SQL前端 XSS框架默认转义但别把口子打开路径穿越黑名单过滤是最典型的看似修了第二层身份边界——谁能操作密钥硬编码AI 最省事的写法鉴权缺失与默认拒绝第三层数据边界——数据往哪流SSRF服务端去请求用户给的 URL敏感数据与日志泄露三条边界一条审查流程安全审查清单与决策表这套框架的边界总结一下一个逻辑全对的接口安全测试一测就穿前阵子让 AI 写了一批接口其中一个商品详情和导出报表。我 review 的时候逐行看过逻辑都对——参数校验有、异常处理有、返回结构正常。结果安全测试一跑第一个接口就穿了。是那种最不起眼的穿法代码没报错功能正常但测试传了个不属于自己的 id照样把数据拉出来了。我当时第一反应是这不怪 AI 吧是我没让它做鉴权。但静下来想问题没那么简单。我 review 人代码的习惯是看逻辑对不对、边界处理了没、异常捕获了没。这套方法 review 人代码很有效因为人的 bug 就出在这几个地方。但到了 AI 代码我慢慢意识到安全漏洞不是写错了是AI 不知道你没告诉它的信任边界。这个认知我是在第四弹 review 那篇文章里想明白一半的——AI 代码的 bug 分写错了和不知道两种。安全漏洞几乎全是不知道那一类AI 不知道输入会从哪进来、不知道这个资源该归谁、不知道数据有多敏感。安全审查画的是信任边界先纠正一个常见的误区审查 AI 代码的安全性不是逐行读代码。逻辑审查和代码安全性审查是两个不同的动作——逻辑审查看的是代码内部对不对安全审查看的是代码和外部世界之间的边界在哪。审查维度逻辑审查安全审查看什么分支、索引、变量、异常输入来源、身份归属、数据流向核心问题这段代码内部对不对边界外的人/数据能不能进来关注点代码本身代码与外部世界的接口AI 盲区相对少最多——AI 看不到上下文我把安全审查拆成三条信任边界对应数据怎么进来、谁能操作、数据往哪流外部世界 ── [输入边界] ── 业务代码 ── [数据边界] ── 外部世界 │ └── [身份边界]谁在调用、资源归谁接下来的三层每层我都按AI 生成时的典型漏洞形态 → 怎么审 → 一个看似修了其实没修的反例来讲。前面三个都是 AI 生成代码时最常见的安全坑我自己也都踩过。第一层输入边界——数据从哪进来这一层是注入类漏洞的高发区SQL 注入、命令注入、XSS、路径穿越。共同点是外部输入被直接当成代码或路径的一部分用掉了。AI 生成时的典型形态动态查询条件拼进原生 SQL很多人以为 2026 年没人手拼 SQL 了都用 ORM。这句话对了一半——用 ORM 的框架层是参数化的但 AI 生成动态搜索/动态排序这类功能时经常会走 ORM 的原生查询接口把变量直接塞进字符串。下面的示例基于 Node 20 Sequelize 62026 年的常用环境涉及 React / Vue 的 XSS 部分是框架层面的通用写法。AI 第一次给我写按多条件搜索商品时是这样写的// 为什么要看这段这是 AI 生成动态查询时的典型写法变量被直接拼进了原生 SQLimport{sequelize}from./dbasyncfunctionsearchProducts(filters:{name?:string;category?:string;sortBy?:string}){letsqlSELECT * FROM products WHERE 11constparams:any[][]if(filters.name){sqlAND name LIKE %${filters.name}%// ← 用户输入直接拼进去}if(filters.category){sqlAND category ${filters.category}}if(filters.sortBy){sqlORDER BY${filters.sortBy}// ← 排序字段也没做白名单}returnsequelize.query(sql,{type:QueryTypes.SELECT})}// 翻车形态的运行结果 正常调用searchProducts({ name: 手机 }) → SELECT * FROM products WHERE 11 AND name LIKE %手机% 恶意调用searchProducts({ name: OR 11 -- }) → SELECT * FROM products WHERE 11 AND name LIKE %% OR 11 -- % → 11 恒真-- 注释掉后面 → 返回全表数据 ← 注入成功问题出在两个地方一是name、category直接拼进了 SQL二是sortBy这个排序字段也直接拼进去了。先说拼接。这里用sequelize.query传原始 SQL——Sequelize 官方文档把这类写法叫 raw queries——框架的保护机制管不到字符串里的变量。修复应该改成参数化sequelize.query本身支持占位符// 修复版值全部走占位符参数化排序字段走白名单asyncfunctionsearchProducts(filters:{name?:string;category?:string;sortBy?:string}){constwhere:string[][11]constparams:any[][]if(filters.name){where.push(name LIKE ?)params.push(%${filters.name}%)}if(filters.category){where.push(category ?)params.push(filters.category)}// 排序字段不能参数化只能白名单constALLOWED_SORT[name,created_at,price]asconstconstsortByALLOWED_SORT.includes(filters.sortByasany)?filters.sortBy:created_atconstsqlSELECT * FROM products WHERE${where.join( AND )}ORDER BY${sortBy}returnsequelize.query(sql,{replacements:params,type:QueryTypes.SELECT})}// 修复后运行结果 恶意调用searchProducts({ name: OR 11 -- }) → 值走占位符SQL 模板固定 OR 11 -- 被当字面量 → 按字面量做 LIKE 匹配查不到数据返回 [] ← 注入被参数化拦下 恶意调用searchProducts({ sortBy: name); DROP TABLE products;-- }) → sortBy 不在白名单回退到默认 created_at → 返回按 created_at 排序的数据 ← 排序注入被白名单拦下有个点得专门说参数化救不了排序字段和列名。占位符只能替代值ORDER BY ?这种写法数据库不认识。所以凡是 AI 生成按用户传入字段排序的功能排序字段必须走白名单这是参数化覆盖不到的边界。前端 XSS框架默认转义但别把口子打开前端这块 AI 也容易踩。React 默认对绑定内容转义Vue 也是所以普通的{data}插值不会 XSS。风险在显式打开的口子——dangerouslySetInnerHTMLReact和v-htmlVue。AI 生成渲染富文本/渲染 AI 返回的 HTML 片段这类需求时经常直接上dangerouslySetInnerHTML因为它省事// 为什么要看这段AI 渲染富文本的典型写法把 AI 返回的内容直接当 HTMLfunctionRichContent({html}:{html:string}){returndiv dangerouslySetInnerHTML{{__html:html}}/}// 翻车形态 正常内容p这是一段说明/p 恶意内容img srcx onerroralert(document.cookie) → dangerouslySetInnerHTML 原样渲染onerror 被触发onerror只是最轻的例子真正严重的是脚本能读取同源接口的 cookie、token。MDN 的 innerHTML 文档对这块有专门的安全说明。修复不是换个框架而是用渲染富文本之前先做白名单化过滤只允许白名单标签和属性去掉on*事件和javascript:协议或者干脆渲染成纯文本。审查时只要看到dangerouslySetInnerHTML/v-html就停下来问一句这个 HTML 的来源可信吗路径穿越黑名单过滤是最典型的看似修了这块我要单独拿出来讲因为它是我见过 AI 修得最假的一种。AI 生成根据用户上传的文件名下载文件功能如果直接把文件名拼进路径读文件就是路径穿越// 为什么要看这段文件名直接拼路径用户传 ../../ 就能读到任意文件constfsrequire(fs)constpathrequire(path)functionreadFileByName(name:string){constfullPathpath.join(UPLOAD_DIR,name)returnfs.readFileSync(fullPath)}// 翻车形态 正常调用readFileByName(report.pdf) 恶意调用readFileByName(../../../../etc/passwd) → path.join(UPLOAD_DIR, ../../../../etc/passwd) 落到 UPLOAD_DIR 之外 → 读到 /etc/passwd我见过 AI 的修复是加了一行name.replace(/\.\.\//g, )把../删掉。这个看着是修了实际上黑名单永远可以被绕过——攻击者用....//、URL 编码、或者..%2f甚至直接用绝对路径都能绕过去。// 看似修了其实没修 readFileByName(....//....//etc/passwd) → replace(/\.\.\//g,) 删掉 .. 后剩 ../还是越界 → 依然读到 /etc/passwd ← 黑名单过滤被绕过正确的修法是白名单 路径规范化OWASP 对路径穿越的说明也强调这一点先把路径 resolve 成绝对路径再校验它是不是落在允许目录里不在就拒绝// 修复版规范化后校验是否落在允许目录内functionreadFileByName(name:string){constfullPathpath.resolve(UPLOAD_DIR,name)// 先规范化成绝对路径constallowedpath.resolve(UPLOAD_DIR)// 允许目录的绝对路径if(!fullPath.startsWith(allowedpath.sep)){// 必须落在目录内thrownewError(非法路径)}returnfs.readFileSync(fullPath)}// 修复后运行结果 恶意调用readFileByName(../../../../etc/passwd) → resolve 后得到 UPLOAD_DIR 之外的绝对路径 → 不在允许目录前缀内 → 抛 非法路径 ← 路径穿越被拦下输入边界这一层审查的时候记住一句话凡是外部输入要进入代码、SQL、路径、命令这几个位置都要问一句它有没有被当成代码/指令用了。参数化、白名单、规范化是这层的三个动作。第二层身份边界——谁能操作这一层是鉴权、授权、密钥管理。AI 的盲区在于它不知道这个操作该由谁来执行、“这个资源该归谁”。我按三个小点来拆谁能进来鉴权→ 谁的资源授权→ 用什么身份密钥。密钥硬编码AI 最省事的写法AI 生成调用第三方服务支付、地图、LLM API的代码时很自然的会把 key 直接写进去// 为什么要看这段AI 生成第三方调用时的典型写法key 写死在代码里constapiKeysk-xxxxxxxxxxxxxxxxxxxx// ← 硬编码constresawaitfetch(https://api.example.com/llm,{headers:{Authorization:Bearer${apiKey}}})// 翻车形态 这段代码一旦提交到仓库/被打进前端包 → git 历史里永久留下 key → 前端包任何人都能解出来 → key 泄露被刷爆账单修复是把 key 挪到环境变量别写进代码// 修复版key 从环境变量注入不进代码constapiKeyprocess.env.LLM_API_KEYif(!apiKey)thrownewError(缺少 LLM_API_KEY 环境变量)// 修复后运行结果 本地.env 里配置 LLM_API_KEYsk-xxx代码里没有 key CI部署时注入环境变量密钥不进 git、不进包但这里有个前端特有的坑我踩过只要 key 最终要跑到浏览器里环境变量也救不了。因为前端代码打包后环境变量会被构建工具直接内联进 JS 文件用户在浏览器里按一下 F12 就能看到。所以凡是前端的调用密钥必须走后端代理——浏览器只跟自己的后端通信后端再拿 key 去调第三方key 永远只待在后端。// 前端密钥的正确做法 浏览器 ── 自己的后端 ── 第三方API │ key 只在这里 前端不持有任何第三方 key鉴权缺失与默认拒绝AI 生成接口时经常能查到数据就觉得该返回。这个我在陪跑文里完整展开一个导出接口的越权事故这里只点一句审查方法对每个接口问三句——谁在调用这个资源归谁没有鉴权会怎样审查时要养成默认拒绝的视角接口默认不允许只有明确加了校验才放行而不是反过来。第三层数据边界——数据往哪流这一层是数据流出类的漏洞SSRF、敏感数据泄露、日志泄露。核心是追踪数据能不能被带出信任边界。SSRF服务端去请求用户给的 URLAI 生成根据 URL 抓取内容导入图片/链接这类功能时经常让服务端直接请求用户传入的 URL。这就是 SSRF——攻击者能让你的服务器去访问内网服务。// 为什么要看这段服务端直接请求用户 URL是 SSRF 的典型形态constresawaitfetch(url)// ← url 是用户传进来的consthtmlawaitres.text()// ... 解析 html// 翻车形态 恶意调用fetch(http://127.0.0.1:6379/) → 内网 Redis 恶意调用fetch(http://169.254.169.254/latest/...) → 云厂商元数据可能拿到临时密钥修复要做白名单 内网拦截而且白名单不是加个域名列表就完事——内网 IP 有很多伪装手法localhost、127.0.0.1、内网网段、甚至 DNS 重绑定都能绕过看起来是外网域名的校验// 修复版解析出 IP 后先拦截内网/保留地址再走域名白名单functionisSafeUrl(rawUrl:string){constunewURL(rawUrl)if(!ALLOWED_HOSTS.has(u.hostname))returnfalse// 域名白名单constipsawaitdns.lookup(u.hostname,{all:true})returnips.every(({address})!isPrivateIp(address))// 拦截内网 IP}// 修复后运行结果 恶意调用fetch(http://127.0.0.1:6379/) → 域名不在白名单 → 拒绝 恶意调用fetch(http://evil.example.com/) // 解析到 10.0.0.5 内网 → 域名在白名单但解析结果是内网 IP → 拒绝 ← 内网拦截兜住敏感数据与日志泄露这个相对好理解但 AI 很爱犯把整个请求体、查询结果直接打日志。AI 生成的日志代码经常是console.log(用户, user)或者把整个请求对象打出来里面带着手机号、token。// 为什么要看这段AI 打日志的典型写法整个对象直接输出console.log(查询订单:,JSON.stringify(order))// ← order 里可能有用户手机号// 翻车形态 日志里出现{orderId:1024,phone:138****1234,card:6222********} → 生产日志被拉走/泄露就是敏感数据泄露修复是日志脱敏只打必要字段敏感字段打码。审查的时候凡是看到JSON.stringify(整个对象)的日志都问一句这个对象里有没有不该出现的字段。三条边界一条审查流程把这三层串起来就是我现在的审查流程。跟四弹的列假设→验证假设→修复假设不同安全审查我直接按边界扫拿到 AI 生成的代码 ├─ 1. 画信任边界哪些是外部输入谁能调用数据要出去吗 ├─ 2. 扫输入边界输入有没有进 SQL/路径/命令/HTML→ 参数化/白名单/规范化 ├─ 3. 扫身份边界接口有鉴权吗key 硬编码了吗资源归属校验了吗 └─ 4. 扫数据边界数据会被带出边界吗日志/响应里有敏感字段吗这套流程不是纸上谈兵。我拿着它回头扫了一遍自己之前让 AI 写的代码第一遍就翻出两处硬编码的 key、一处没做归属校验的接口还有一处把整个请求对象打进日志的。之前用看逻辑对不对的旧方法这些全漏了——因为它们逻辑都对。安全审查清单与决策表这是我写文章时整理出来的清单去掉了项目敏感信息。审 AI 生成的代码我按这个扫安全检查清单审 AI 代码前扫一遍输入边界有没有外部输入拼进 SQL/命令/路径/HTML→ 参数化 / 白名单 / 规范化输入边界排序字段、列名这类不能参数化的有没有白名单输入边界dangerouslySetInnerHTML/v-html用了没来源可信吗输入边界路径读取有没有 resolve 后校验落在允许目录内身份边界每个接口问谁在调用 / 资源归谁 / 没鉴权会怎样身份边界有没有硬编码的 key / token身份边界key 会不会打进前端 bundle→ 必须走后端代理数据边界服务端有没有请求用户提供的 URL→ 白名单 内网拦截数据边界日志 / 响应里有没有打出敏感字段兜底高危接口支付/权限/数据导出有没有上扫描工具 人工审重点查哪层决策表代码类型优先查的边界为什么搜索 / 查询 / 动态条件输入边界注入高发排序字段白名单最容易漏富文本 / 内容渲染输入边界XSSdangerouslySetInnerHTML/v-html文件读写 / 上传下载输入边界路径穿越黑名单过滤必被绕接口 / 导出 / 报表身份边界越权 / IDOR 高发第三方服务调用身份边界密钥硬编码、前端 key 暴露抓取 / 导入 URL数据边界SSRF内网/元数据泄露日志 / 监控埋点数据边界敏感字段泄露这套框架的边界先说清楚这套审查覆盖不到什么别把它当成安全万能药。不覆盖依赖漏洞。第三方库的 CVE比如某个 npm 包有已知漏洞审查 AI 生成的代码本身是看不出来的。这要依赖扫描工具npm audit、Snyk和依赖治理不在信任边界的范畴。不覆盖基础设施和部署层。云厂商配置、开放端口、数据库权限是另一层的事。代码再安全配置错了一样漏。不覆盖业务逻辑安全。比如风控、频率限制、薅羊毛、并发竞态这类靠信任边界这个视角看不到需要专门的业务安全设计。工具能兜多少静态扫描工具Semgrep、SonarQube能抓语法级问题——硬编码密钥、明显的字符串拼接 SQL。但抓不了语义级问题——越权、IDOR、业务逻辑漏洞因为那是这个数据该不该给这个人的语义判断工具没有业务上下文。这也是为什么不能只靠工具要人工按信任边界审。工具抓写错了信任边界审不知道正好互补。上面的三条边界对应 OWASP Top 102021 里的注入A03、失效的访问控制A01、服务端请求伪造A10——框架有据可查不是我自己编出来的分类。清单是审查起点不是安全证明。高危场景支付、权限、数据导出即使过了这个清单也该上专业工具 渗透测试。这个清单的价值是把最容易漏的那几类固定下来不是替代完整的安全流程。总结一下回头看这弹在系列里的位置。防线告诉你怎么兜底信任分级告诉你哪些代码敢放测试策略告诉你写完了怎么测review 告诉你你怎么审逻辑。这一弹补上的是最后一块审 AI 代码除了审逻辑对不对还要审信任边界在哪。安全漏洞是 AI 上下文盲区的高发区——它不知道输入会从哪来、资源归谁、数据多敏感。你 review 的时候把这三条边界过一遍能把 AI 代码里最常埋的几个洞堵掉一大半。本文涉及的相关文章AI 代码 Review 实战从看代码对不对到看 AI 懂不懂AI 代码信任分级实战从全信到三档我在哪些代码上敢让 AI 自己跑AI 代码测试分层实战从 A 档到 C 档AI 写的代码我这么测