☰
代码生成器安全:从OpenAPI输入到供应链攻击的防御指南
2026/9/29 10:24:41 网站建设 项目流程

前端同事群里那个安全公告链接发出来的时候,我第一反应是没太在意。Orval 这个开源工具在 TypeScript 生态里太常见了,它专门负责把 OpenAPI(也就是 Swagger)文档自动生成 API 客户端代码,日常开发中每天都在用,也几乎没听说过它出什么安全问题。直到我把公告完整读了一遍,才意识到这次和普通依赖漏洞完全不同——代码注入漏洞落在代码生成器上,性质就变了:Orval 解析的是外部文档,输出的却是会被直接编译进业务线的源码,中间任何一个拼接点被攻破,等于把上游喂进来的内容变成了你仓库里的"合法代码"。这就是典型的供应链攻击通道,而且比投毒 npm 包更隐蔽。

这篇文章我按自己实际排查和整改的路径来写,先讲为什么生成器类工具的风险模型特殊,再拆攻击触发链路,然后给出一套可执行的自查清单,最后是三层加固方案和团队防护基线。前端负责人、DevOps、以及做安全合规的同事都可以直接拿去参考。

1. 一个"写代码的工具"被攻破,为什么比普通依赖更麻烦

1.1 Orval 到底在做什么

Orval 的核心能力一句话就能说清:读 OpenAPI 文档,生成 TypeScript 类型定义、API 请求函数、React Query / Vue Query 的 hooks,甚至还能生成 mock 服务。团队里只要维护一份 OpenAPI 契约文件,就能在几秒内得到一套类型安全的接口调用层,业务代码直接 import 使用,不再需要手工维护任何请求代码。

典型配置长这样:

// orval.config.ts import { defineConfig } from 'orval'; export default defineConfig({ petstore: { input: './openapi.yaml', output: { target: './src/api/generated', client: 'react-query', mode: 'tags-split', }, }, });

跑完npx orval之后,src/api/generated目录下会出现一批.ts文件,里面有完整的接口类型和调用封装。这个工具替团队省掉了大量重复劳动,类型定义和接口文档始终一致,平时确实是"用了就回不去"的效率利器。

1.2 代码生成器的信任边界和普通依赖不一样

普通 npm 依赖被引入项目后,是在运行时才被 import 的,漏洞影响的是运行逻辑。代码生成器完全不同:它的输入——OpenAPI 文件——本质上是外部数据,输出——生成的.ts源码——本质上是你们仓库内的一部分。而生成动作通常发生在开发机或 CI 流水线里,全程自动化,几乎没有人会逐行审查产物。

这个信任模型有个致命点:如果注入成功,恶意代码不是以"第三方依赖黑盒"的形式存在,而是变成了你们仓库里明晃晃的源码。它会跟着代码评审流程、跟着编译、跟着测试、最后进制品发布。普通漏洞还需要想办法绕过沙箱或提权,这种攻击直接落在"你们自己的代码"里,优先级完全不同。

打个比方你就明白了:用模板引擎拼 HTML 时,如果不转义就插入用户输入,会形成 XSS。代码生成器就是把外部文档插入到.ts/.tsx文件里,如果不按语言语义做转义,就是代码注入。区别在于,这里被注入的代码会被编译并跟随产品一起发布,危害半径比前端 XSS 大得多。

1.3 不容易被注意到的攻击面

有几个现实因素放大了这个漏洞的影响:

  • OpenAPI 文档经常是第三方提供的。B2B 平台、云厂商、API 聚合服务商,都会直接给你一份 swagger 文件,或者给你一个 URL 让你们自己拉取。
  • CI 流水线里通常会自动执行orval generate。很多人觉得"生成代码而已,又不需要人看",于是把这一步做成纯自动。
  • 生成产物要么被提交进仓库后没人认真 review,要么干脆被.gitignore掉,反正随时能重新生成。
  • Orval 基本是devDependencies,而大部分团队的依赖安全审查重点都放在运行时依赖上,开发依赖长期处于"没人管"的状态。

这些因素叠加在一起,让"上游文档被污染 → 生成器产出恶意代码 → 恶意代码进入生产"这条链路异常顺畅。所以这次公告虽然标题写的是 Orval 存在代码注入漏洞,本质上真正的问题是我们对生成器类工具的安全假设太乐观了。

2. 触发链路:恶意接口文档如何一步步变成你仓库里的源码

2.1 先弄清楚数据流

要理解代码注入,得先看清楚 OpenAPI 文档里有哪些内容会进入最终生成的代码。以 Orval 这类生成器为例,至少下面这些字段都会出现在产物里:

字段生成产物中的位置潜在注入上下文
operationId函数名、变量名标识符上下文,可破坏语法结构
path请求 URL 模板模板字符串上下文
descriptionJSDoc 注释注释块可被*/提前闭合
summary注释、枚举说明同上
tags分组文件名、注释文件路径 + 注释
x-*扩展字段自定义输出逻辑取决于生成器的处理方式
enum值联合类型字面量字符串字面量上下文

这七个位置每一个都是字符串拼接点,也就是潜在的注入点。很多生成器模板内部长这样:

// 生成器内部简化示意 const output = ` /** * ${spec.description} */ export const ${spec.operationId} = async (params: ${spec.paramsType}) => { return request.${spec.method}(\`${spec.path}\`, { ...params }); }; `;

这段代码看着无害,但问题恰恰出在${}插值上——如果spec.description或spec.operationId的内容没有经过针对性的过滤,外部输入就能改变最终文件的语义。

2.2 一个能说明问题的简化示例

假设生成器把description原样写进生成的注释里。正常情况是:

description: "获取用户列表"

生成:

/** * 获取用户列表 */

但如果上游文档被污染,description变成了这样:

description: "正常描述 */ console.log('pwned') /* 继续注释"

拼出来的产物就变成了:

/** * 正常描述 */ console.log('pwned') /* 继续注释 */

注释块在*/处提前闭合,console.log('pwned')变成了真正会被执行的语句。这只是最基础的演示,实际攻击中攻击者可以注入读取环境变量、向外发送 HTTP 请求、甚至拉起子进程的代码。同理,如果operationId没做标识符合法性校验,或者path被拼进模板字符串,也存在同类逃逸的可能。

需要说明的是,我这里展示的是代码生成器这一类工具的通病触发形态,用来解释原理。这次 Orval 公告里具体的触发字段、受影响配置和官方评级,请以官方 advisory 和修复版本 release notes 为准。重点是:这类漏洞不依赖"某个工具写得烂",而是代码生成器的输出上下文实在太多样——字符串字面量、模板字符串、注释、标识符、正则表达式,每一种上下文都需要不同的转义规则,开发时极难全部考虑到。

2.3 从代码注入到执行的路径很短

代码注入不等于立刻拿到远程命令执行,但在生成器场景里,这条路径比想象中短得多。

第一,如果注入位置在注释闭合处或函数体内部,那么 CI 构建阶段就可能触发。很多项目的构建流程里有lint --fix、tsc、单测前的代码加载,这些环节都会执行源码内容,注入的语句会跟着跑。

第二,如果注入的是运行时逻辑,比如请求拦截器、错误处理分支、token 刷新逻辑,那么应用一上线、用户一访问相关接口就会触发。攻击者完全可以把 payload 设计成"看起来像正常业务逻辑",在生成代码里埋一个发走私流量的定时器。

第三,构建机的环境变量往往包含云服务密钥、私有仓库 token、发布凭据。注入代码如果读取process.env并外传,攻击者实际上拿到了构建环境的部分能力,后续横向移动就有了立足点。

所以说"代码注入"四个字,实际是给了攻击者一座从恶意文档通向代码执行的桥。桥的这头是上游一份不起眼的 YAML,桥那头是你们的生产代码。

2.4 为什么它会被定性为供应链风险

供应链攻击的定义核心是:你把信任交给上游,上游被污染,下游批量遭殃。Orval 的输入直接来自第三方契约文档,而且同一个 OpenAPI spec 经常被几十甚至上百家公司拿去做客户端生成。恶意文档只需要被构造一次,上传到公共接口文档站点或者通过某个被攻破的平台下发,就能同时污染所有拿它生成客户端的仓库。

和直接投毒 npm 包相比,利用代码生成器的攻击更隐蔽。npm 包投毒至少还会在package.json里多出一个依赖项,或者 lockfile 里多出明显的可疑包,安全扫描容易发现。而生成器攻击的产物是你们仓库里本该存在的.ts文件,diff 里可能只有几行"普通代码"的变化,reviewer 默认跳过了生成目录,安全扫描默认不打生成代码,就这样一路绿灯进了生产。

供应链风险这个定性,一点都不夸张。

3. 影响面自查:三分钟内确认你的项目是否踩线

公告出来后,团队第一时间要做的是自查,不是盲目升级。以下四步是我这次实际执行的排查路径,照着做基本能把影响面摸清楚。

3.1 第一步:锁定生成器版本

先把项目里 Orval 的版本和所有相关子包查出来:

grep -n '"orval"' package.json package-lock.json pnpm-lock.yaml yarn.lock

注意 Orval 生态里有不少配套包,比如@orval/core、@orval/query、@orval/axios,都要一起查:

grep -nE '(@orval/|"orval")' package-lock.json | head -50

拿到版本号后,对照官方公告里的受影响版本区间判断是否踩线。同时用npm view orval versions --json看一眼最新版本,确认当前可升级到的目标版本。这里有个容易忽略的细节:lockfile 才是真相来源。package.json里写的^5.0.0不代表实际安装的版本,如果团队里有人的 node_modules 是旧的,而 CI 里做了缓存,版本可能五花八门。以 lockfile 为准,并且要求所有环境重新安装。

3.2 第二步:检查生成产物

找到orval.config.ts里配置的output.target目录,直接对生成目录做一次特征检索。常见的有害模式包括:

grep -nE "eval\(|new Function|process\.env|atob\(|Buffer\.from|fromCharCode|child_process" ./src/api/generated -r

上面这些是显眼的,更隐蔽的还要查模板字符串里是否混入了非预期的${...}表达式。正常的生成代码里,变量插值通常是params.id这种符合预期的参数占位;如果出现${\...`}嵌套、${globalThis...}、${require...}` 之类的写法,基本可以认定有问题。

另外一定要看生成文件的时间戳和 git 历史。如果生成目录里的文件最后一次改动时间早于公告日期,而近一周又没有重新跑过生成,说明产物很可能是旧版本生成的、带着潜在问题的代码。把这个时间关系记录清楚,后续判断"是否需要回溯审计"会很有用。

3.3 第三步:追溯 spec 来源

排查完产物,接下来要回答一个问题:喂给生成器的 OpenAPI 文档到底从哪来的?

打开项目里的 spec 文件(可能是openapi.yaml、swagger.json,也可能是配置里的远程 URL),分别看这几项:

  • 文件是内网托管的,还是 CI 里每次构建都从第三方 URL 拉取?
  • 最近几次 git 提交里,spec 文件的变化是否符合契约变更的预期?
  • operationId、description、x-*扩展字段里有没有异常内容?
  • 有没有人通过 PR 直接修改过 spec 里与接口定义无关的字段?

这一步的意义在于判断攻击窗口。如果 spec 一直是内网固定文件,且 git 历史干净,那么实际受影响风险相对低;如果 CI 每次构建都远程拉取最新文档,那任何一次拉取的内容异常都等于把恶意代码接进了构建链路,受影响窗口会大很多。

3.4 第四步:连带检查依赖树

代码生成器的修复可能带来依赖树变化,顺手把依赖审计也做了:

npm audit --audit-level=high # 或者 pnpm audit / yarn audit

重点看 lockfile 中最近新增的传递依赖。攻击者经常用 typosquatting 手法,把恶意包名伪装成orval-core、orvl这种跟官方包高度相似的名称。对比升级前后的 lockfile diff,凡是跟本次版本变更无关的新包,都要逐一确认来源。

另外,如果团队里有多个仓库共用同一份 spec 或同一套生成配置,同步做完上述四步,别只查一个项目就收工。代码生成器的风险是横向扩散的,查漏一个仓库等于没查。

4. 修复不是只升级版本:从生成器到产物到 CI 的三层加固

4.1 第一层:升级、固定、做 diff

修复的第一步永远是升级到官方修复版本,这没什么好犹豫的:

npm install -D orval@latest # 或安装公告指定的修复版本 npm install -D orval@5.2.1

升级之后,关键动作是重新生成并且做一次完整的git diff,不能把"升级完了"当成结论。原因很直接:生成器版本变化可能带来输出格式调整、类型推断差异、代码组织方式变化,这些变化本身就需要 review。如果直接覆盖生成产物的目录然后提交,等于把一堆未经确认的代码悄悄合入主干。

我建议的流程是:

npx orval git diff --stat git diff src/api/generated

先看统计信息,再看具体内容。正常的一次 spec 更新只应该影响相关模块;如果整个生成目录上百个文件全变了,说明是模板级别的大改动,务必逐块确认。确认没问题后,把生成产物目录的文件 hash 记下来,作为后续基线:

find src/api/generated -type f -exec md5sum {} \; | sort > generated-files.sha256

这份基线保存在仓库里,以后每次升级或 spec 变更都能快速对比,谁改了什么都不用靠记忆。

4.2 第二层:把 OpenAPI 文档当外部输入处理

升级版本只是堵住了已知漏洞,但生成器类工具的根问题是"外部输入直接进源码"。所以这一步要做的是:把 OpenAPI 文档当成不可信的外部输入,在进入生成器之前增加校验和消毒环节。

首先,生成前跑一遍结构校验:

npx @redocly/cli lint openapi.yaml # 或者 npx spectral lint openapi.yaml

结构校验能拦截掉格式破损、类型错误的文档,但要注意:它拦不住代码注入,因为注入 payload 往往是完全合法的字符串。所以还要配合内容基线管理。

具体的做法是:对高风险字段建立白名单和基线。比如x-*扩展字段——OpenAPI 规范允许自定义扩展,生成器接触到的每一个x-字段都必须有明确的用途说明,没有说明的一律不允许出现在 spec 里。operationId、description、summary这些字段,要求只能来自契约变更评审,任何"非契约性改动"都要拦截。

很多团队做不到这一步,是因为 spec 文件就散落在各个业务仓库里,来源没人管理。我的建议是:建一个独立的 spec 仓库,所有第三方 OpenAPI 文档统一收管,每个文件记录来源 URL、获取时间、内容 hash。生成器只从这个受控仓库读取文件,不允许业务代码直接拉取第三方 URL。这样即使上游文档被污染,我们也能第一时间通过 hash 对比发现差异。

4.3 第三层:CI 和产物防护

如果注入最终触发在了 CI 构建阶段,攻击者能拿到多少东西,取决于生成这一步的环境里挂载了多少凭据。所以 CI 侧要做几件强制的事:

第一,生成步骤和环境里不要挂载不必要的 secret。尤其是云平台 token、发布凭据,只在真正需要的 stage 注入,生成代码这个环节通常是纯文本处理,不涉及任何密钥读取,挂了就是风险的放大器。

第二,把"生成产物 diff 异常"设为失败条件。正常一次 spec 更新不会让几百个文件全变。可以在 CI 里加一个脚本,对比上次构建的产物 hash;如果产物里出现了与本次 spec 变更无关的新代码,流水线直接红掉,等待人工确认。

第三,把生成目录纳入代码扫描范围。别再用"生成代码不看"的旧习惯。Semgrep、CodeQL 都可以针对生成目录单独跑规则,重点查eval、Function构造器、可执行字符串拼接、异常外联请求等特征。扫描规则可以宽松一些,宁可误报也不能漏报。

第四,在生成产物的基础上生成 SBOM 记录。每个产物文件的 hash、生成器版本、spec 版本、变更时间都记录下来。出问题的时候,回溯"这个文件是谁在什么版本下生成的"几秒钟就能定位。

4.4 如果已经确认被利用,怎么办

发现确认被利用,别慌,按紧急程度处理:

  • 立即把生成产物回滚到上次通过审查的 commit,同时把生成器固定到干净版本,停掉 CI 里的自动生成任务。
  • 轮换 CI 环境变量里所有可能被读取的凭据。假设密码、token 已经泄露,不要赌"应该没被读到"。
  • 回溯受影响窗口期内所有构建日志和产物,重点查有没有异常的外联请求。构建日志里通常会记录网络行为,拿这些日志去对照已知的可疑域名或 IP。
  • 把事件写成记录提交给安全团队,不要只当版本事故处理。代码注入事件的价值在于复盘攻击路径,留档对后续防护有直接帮助。

5. 供应链安全的一次复盘:代码生成环节为什么是盲区

5.1 团队普遍存在的三个盲区

这次事件之后我复盘了一下,发现代码生成环节成为供应链盲区,基本可以归结为三个原因。

第一个盲区是依赖治理的范围太窄。绝大多数团队的依赖安全清单只覆盖dependencies,devDependencies里的生成器、脚手架、lint 工具基本没人看。但恰恰是这些工具拥有最大的"代码写入权"——运行时依赖出问题影响的是功能,开发依赖出问题影响的是整个仓库的代码完整性,后者的破坏半径更大。

第二个盲区是"官方工具"的信任惯性。Orval 这种工具每天都在被大量项目使用,生成出来的代码格式规范、类型准确、测试通过,时间长了团队就默认"官方生成的东西不会有问题",review 时直接跳过。这种信任惯性在大多数时候是效率之源,但安全上恰恰是它给了攻击者可乘之机。

第三个盲区是生成产物被习惯性排除在代码评审外。开发者改的是 spec,不是代码,reviewer 默认"改了文档不会影响逻辑",再加上生成目录文件多、内容杂,谁都不愿意逐行看。结果就是恶意代码可能已经在仓库里躺了好几周,没有任何人真正读过它。

5.2 我们这次建立的基本盘

针对上面的盲区,团队这次建立了一套相对完整的防护基线,分享出来供参考:

  • 工具分级管理:把代码生成器、脚手架、构建工具列入与运行时依赖同等级别的资产清单,统一锁定版本,统一升级流程。
  • 漏洞通告订阅:GitHub Advisory Database、npm Advisory、以及相关社区的消息源,指定专人每周扫描一次,有新通告立刻评估影响。
  • 快速响应预案:所有仓库统一固定 Orval 版本,公告出来后保证一小时内完成全量升级、重新生成、产物 diff 三步。
  • 双人复核机制:生成产物里任何非预期的代码变更,必须有生成器维护者和业务负责人双人确认。
  • spec 统一收管:第三方 OpenAPI 文档统一存放到独立仓库,记录来源、获取时间、内容 hash,杜绝"CI 直接拉第三方 URL"这种裸奔做法。

这套东西不复杂,但每一条都是这次事件里真实踩过之后得出的。前四条大多数团队三两天就能落地,第五条可能需要推动一下,但收益是长期的。

5.3 一个落地小技巧

最后分享一个我实操中觉得特别好用的技巧:把生成目录放到独立的 git 子模块里维护,不要和业务代码混在同一个 commit 里。

这样做的好处是 diff 对象非常清晰。每次升级生成器、变更 spec、更新产物,都是独立的 commit,三者一一对应。排查问题的时候看一眼历史就能定位:是哪次生成器升级引入了行为变化,哪次 spec 变更让某些文件变多了,哪次产物更新没有对应任何 spec 变更。没有子模块隔离时,生成器和产物混在业务提交里,回溯起来基本靠猜。

我个人这次最大的体会就是:生成代码也是代码,它不该被豁免审查,也不该被当成黑盒信任。Orval 的漏洞只是把代码生成器这类工具的共性问题摆到了台面上,类似的工具还有很多——只要是"读外部输入、写仓库源码"的环节,都存在同一个风险模型。与其等下一次公告出来再手忙脚乱地自查,不如现在就把上面的检查清单固化成团队流程,每次生成器升级、每次 spec 变更都按流程走一遍。成本很低,但真到出事那天,你会庆幸自己提前做了这些。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询