Skip to content

Vite + React + TypeScript 应用遭遇恶意生产提交的安全影响 ​

译注:本文编译自 dev.to 作者 Marco Altomare 的安全研究文章,原文探讨了当恶意 commit 进入 Vite 应用并部署到生产环境后,前端团队可能面临的真实风险边界。原文链接见文末。

核心结论 ​

大多数前端团队直到出事才会思考一个问题:如果恶意 commit 进入 Vite 应用并成功上线,究竟会发生什么?答案远不止“界面崩了”这么简单。在现代 React + TypeScript 技术栈中,被污染的前端可以观察用户输入、读取浏览器可访问的数据、调用已认证的 API、篡改业务流程,甚至通过 Service Worker 实现持久化驻留。一旦恶意代码被交付给用户,浏览器本身就成为了攻击面的一部分。

研究给出的判断是:如果攻击者能修改 Vite + TypeScript + React 仓库并让改动被部署,就应当假定该应用的前端完整性已经丧失。 攻击者的 JavaScript 会在每个受影响访客的浏览器中,以网站源(origin)的身份运行,拥有与合法 React 应用基本相同的浏览器权限。

四个信任区与风险分层 ​

文章将系统划分为四个信任区:源码控制、构建/CI、浏览器、后端服务。攻击者控制不同区域,后果差异明显:

  • 仅仓库读取权限:可读取源码与历史,但未必能影响用户。
  • 可合并或推送可部署代码:可将恶意行为植入源码、配置、依赖、工作流或公共资源。
  • 成功部署到生产:加载该版本的访客会执行恶意客户端 bundle。
  • CI 特权执行:构建期代码可能按工作流权限访问 CI 密钥、部署凭证、runner、产物或内部服务。
  • 后端被攻陷:这并非自动发生,取决于仓库是否含后端代码、CI 是否暴露后端凭证、公开嵌入的凭证是否权限过大,以及 API 是否强制鉴权。

主要攻击路径 ​

直接修改源码:高价值目标包括 src/main.tsx、src/App.tsx、路由与布局组件、认证 Provider、全局状态与 API 客户端;登录、注册、结账、上传等表单组件;被广泛复用的 shadcn/ui 封装组件(改动一个 Input 或 Button 即可影响大量页面);以及 index.html 和 public/ 目录。React 的 JSX 转义对“故意提交的恶意代码”毫无意义,dangerouslySetInnerHTML 等机制尤其需要警惕。

Vite 配置与插件:vite.config.ts 是敏感文件。Vite 插件可参与源码解析、转换、HTML 转换与打包,transformIndexHtml 能在构建时修改入口 HTML。恶意插件可绕过 React 组件直接注入代码,还可通过 apply 设置只在生产或只在 CI 环境生效,使本地开发看起来一切正常。

依赖与 lockfile 篡改:npm 的 scripts 字段允许任意命令,preinstall、install、postinstall、prepare 等生命周期钩子会在安装时执行,代码运行在开发者或 CI 机器上而非浏览器中。建议使用提交的 lockfile 配合 npm ci,但这只能提升可复现性,不能证明锁定代码是良性的。

CI/CD 工作流:最高风险模式是在特权上下文中执行不可信代码。pull_request_target 会以基础仓库的 token 和潜在密钥运行,若再检出并执行 PR 代码,凭证就可能泄露。自托管 runner 若持久化或能访问内网,影响会进一步放大。

外部脚本与 Service Worker:第三方脚本服务器被攻陷是 OWASP 列出的主要风险之一。恶意部署还可注册或替换 Service Worker,拦截页面与子资源请求。这意味着回滚源码并重新部署,未必能立即清除已访问浏览器中的恶意缓存或活动注册,排查时需检查注册、CacheStorage 与 PWA 更新行为。

浏览器中可被窃取的数据 ​

恶意一方 JavaScript 可观察受影响页面上的输入与交互事件,包括用户名、邮箱、密码、密码重置值、个人信息、私信、支付卡字段、一次性验证码,甚至用户取消提交前的表单内容。这类针对支付界面的攻击常被称为 web skimming。

脚本还可读取 DOM 与 JavaScript 状态中的信息:已认证用户的姓名、角色、账号、表格与仪表盘数据、CSRF token、API 返回数据、路由与查询参数,以及 React context、store、query cache 中的值。恶意提交不需要“攻破 React”,它本身就是 React 的一部分。

浏览器存储方面,localStorage、sessionStorage、IndexedDB、可被 JS 读取的 Cookie、Cache Storage 均可被同源脚本读写。OWASP 建议不要在 localStorage 中存放敏感信息或会话标识。HttpOnly Cookie 无法通过 document.cookie 读取,但这并不使被污染的页面安全——同源恶意代码仍可发起浏览器自动携带 Cookie 的已认证请求。

通常无法窃取的内容 ​

被污染的前端虽然强大,但并不等同于操作系统级恶意软件。在常规浏览器安全边界下,它通常无法:读取用户未选择或未授权的磁盘文件;读取其他无关站点的 DOM 或存储;读取其他站点的跨域 API 响应(无 CORS 许可时);通过 JS 读取 HttpOnly Cookie 值;在从未授权的源上获取摄像头、麦克风或精确定位;提取浏览器为其他域保存的密码;仅凭控制前端就绕过服务端鉴权。

构建期资产与 Vite 环境变量 ​

构建/部署上下文中若存在以下资产,则面临风险:CI 注入的仓库与组织密钥、GITHUB_TOKEN、云厂商凭证、部署 token、包与容器镜像仓库凭证、SSH 与签名密钥、构建产物与依赖缓存、持久化自托管 runner 上的残留文件,以及可从 runner 网络访问的内部服务。

Vite 的环境变量行为是分析重点:以 VITE_ 开头的变量会被暴露给客户端代码并在构建时静态替换,Vite 明确警告 VITE_* 不得包含密钥。envPrefix 控制暴露范围,空前缀可能导致敏感变量泄露。vite.config.ts 中的 define 也能在无默认前缀的情况下把进程环境值注入客户端 bundle。需要强调的是:重命名密钥、压缩 bundle 或隐藏 source map,都不能让浏览器交付的值变成秘密;一旦凭证被提交,.gitignore 就不再是安全边界,必须立即吊销或轮换。

原文链接 ​

https://dev.to/marco_altomare_0e7674642c/security-impact-of-a-malicious-production-commit-in-a-vite-react-typescript-application-2lf5