Skip to content
Security

保障 npm 安装安全

一本实用指南,适合任何从 npm 安装 Supabase 包的人——无论是 JavaScript 客户端库(@supabase/supabase-js 及其相关库)、supabase CLI,还是你依赖树中的其他依赖——用于防御供应链攻击。大部分内容适用于任何 npm 包,而不仅仅是 Supabase 的。

🌐 A practical guide for anyone installing Supabase packages from npm — the JavaScript client libraries (@supabase/supabase-js and friends), the supabase CLI, or any other dependency in your tree — on defending against supply-chain attacks. Most of it applies to any npm package, not only Supabase's.

这份指南为什么存在 #

🌐 Why this guide exists

同样的攻击模式一直在重复:一个流行的 npm 包被攻破,新的版本通过生命周期脚本或传递依赖在 npm install 上执行攻击者代码,恶意软件从安装主机收集凭证,然后通过重新发布受害者维护的其他包来实现自我传播。

🌐 The same attack pattern keeps recurring: a popular npm package is compromised, the new version executes attacker code on npm install via a lifecycle script or transitive dependency, the malware harvests credentials from the install host, and then self-propagates by republishing other packages the victim maintains.

好消息是:无论任何发布商做什么,大部分影响是消费者可以防范的。这个指南就是我们建议你采用的一系列设置和习惯。

🌐 The good news: most of the impact is preventable from the consumer side, regardless of what any one publisher does. This guide is the set of settings and habits we recommend you adopt.

今天做什么 #

🌐 What to do today

  1. 提交你的锁文件,并在 CI 中使用 --frozen-lockfile (pnpm/yarn) 或 npm ci (npm) 安装。
  2. 隔离新版本。 设置一个最小发布天数(≥ 7 天),这样在攻击者仍在检测窗口时,安装就不会获取全新的版本。npm、pnpm、yarn 和 bun 现在都原生支持这个功能。
  3. 阻止奇特的及物依赖。 拒绝那些没有来自 npm 注册表的 github:git+file: 引用。
  4. 验证来源。 安装后运行 npm audit signatures。Supabase 的包会通过 sigstore attestations 发布(加密证明,将每个 tar 包与构建它的工作流运行、提交和仓库绑定)。
  5. 限制生命周期脚本。 默认拒绝 postinstall / preinstall / prepare;按包允许。
  6. 为你的包管理器固定一个唯一可信来源(在 package.jsonpackageManager 字段中使用 sha512 哈希)。
  7. 准备回滚计划。 如果上游宣布出现安全漏洞,要知道哪些软件包、哪些版本以及哪些凭证需要更换。

本文档的其余部分会详细展开每一个内容,并单独讲解 Edge Functions / Deno 的情况。

🌐 The rest of this document expands on each of these and covers the Edge Functions / Deno case separately.

固定你的依赖版本 #

🌐 Pin your dependency versions

已提交的锁文件是地板,而不是天花板。

🌐 A committed lockfile is the floor, not the ceiling.

应用仓库

  • 提交 package-lock.jsonpnpm-lock.yamlyarn.lockbun.lock
  • 在 CI 中,用 npm ci / pnpm install --frozen-lockfile / yarn install --immutable / bun install --frozen-lockfile 进行安装。如果 package.json 和锁文件不一致,这些会失败,这正是你想要的。
  • 如果你也有锁文件以及下面的其他指导,package.json 中的插入符号范围(^1.2.3)是可以的——实际安装的就是锁文件里的内容。

通过覆盖来固定传递依赖的风险。 如果你不信任某个传递依赖版本,可以通过以下方式强制使用已知的良好版本:

1
// npm and pnpm
2
"overrides": {
3
"some-dep": "1.2.3"
4
}
5
6
// yarn
7
"resolutions": {
8
"some-dep": "1.2.3"
9
}

当你看到一个你不直接依赖的传递依赖上有 CVE 时,这就是可以用的杠杆。

🌐 This is the lever to reach for when you see a CVE on a transitive you don't directly depend on.

小心 npx / pnpm dlx / bunx#

🌐 Beware npx / pnpm dlx / bunx

这些命令会在不使用你项目的锁文件和最低版本限制的情况下获取并运行一个包。npx pkg@latest 是直接从注册表获取,一个新的恶意版本将会被安装。两个实际的缓解方法:

🌐 These commands fetch and run a package outside your project's lockfile and outside your minimum-age gate. npx pkg@latest is a direct fetch against the registry, and a fresh malicious version will be installed. Two practical mitigations:

  • 固定版本:使用 npx pkg@1.2.3 而不是 npx pkg@latest
  • 将工具移动到 devDependencies,这样它就会被你的锁文件和本指南的其余部分覆盖,然后通过 npm exec / pnpm exec / yarn run 调用它。

对待任何临时注册获取,就像对待 curl … | bash 一样。

🌐 Treat any ad-hoc registry fetch the same way you'd treat curl … | bash.

隔离新版本(最小发布日期) #

🌐 Quarantine new versions (minimum release age)

大多数 npm 安全问题都会在几个小时内被发现并处理。对新发布的版本进行短时间隔离是最有效的设置。

🌐 Most npm compromises are detected and remediated within hours. A short quarantine on freshly-published versions is the single highest-leverage setting.

🌐 pnpm (recommended)

pnpm v11 默认开启这个(1440分钟 = 1天)。你可以提高它。在仓库根目录的 pnpm-workspace.yaml 中(pnpm 10+ 无论有没有 workspaces 都会从这个文件读取配置):

🌐 pnpm v11 turns this on by default (1440 minutes = 1 day). You can raise it. In pnpm-workspace.yaml at the repo root (pnpm 10+ reads config from this file with or without workspaces):

1
minimumReleaseAge: 10080 # 7 days, in minutes
2
minimumReleaseAgeExclude:
3
- '@your-org/*' # bypass for your own internal packages

只有在你有特别理由想要放弃默认设置时才设置 minimumReleaseAge: 0

🌐 Set minimumReleaseAge: 0 only if you have a specific reason to opt out of the default.

trustPolicy#

🌐 trustPolicy (pnpm)

独立于年龄限制,pnpm 的 trustPolicy: no-downgrade 会拒绝安装其信任等级(受信任的发布者 → 来源 → 无)相较于同一包的之前版本下降的版本。这可以防止攻击者发布但无法复制原维护者的 OIDC 绑定的情况:

🌐 Independent of the age gate, pnpm's trustPolicy: no-downgrade refuses to install a version whose trust level (trusted publisher → provenance → none) has dropped relative to previous releases of the same package. That catches the case where an attacker can publish but can't replicate the original maintainer's OIDC binding:

1
trustPolicy: no-downgrade
2
trustPolicyExclude:
3
- 'some-package' # opt specific packages out if needed
4
trustPolicyIgnoreAfter: '180d' # ignore checks for packages older than 180 days

yarn#

🌐 yarn (berry / v4+)

.yarnrc.yml 中:

🌐 In .yarnrc.yml:

1
npmMinimalAgeGate: '7d'
2
npmPreapprovedPackages: # opt specific packages out of all package gates
3
- '@your-org/*'

比 gate 更新的版本不会被解析。Yarn 的文档也提到,这可以防止 npm 仓库的 72 小时下架窗口——你最近安装的包如果没等到时间过去就可能消失,从而导致构建失败。

🌐 Versions newer than the gate are excluded from resolution. Yarn's docs also note this guards against the npm registry's 72-hour unpublish window — a package you recently installed could vanish, breaking your build, if you don't wait it out.

.yarnrc.yml 中,有两个相关的 yarn 设置值得了解:

🌐 Two related yarn settings worth knowing about while you're in .yarnrc.yml:

  • enableScripts: false 是 yarn 的默认设置 — 第三方包的 postinstall 脚本不会运行。工作区的脚本仍然会运行。
  • enableHardenedMode: true 让 yarn 重新查询远程注册表,以确认锁文件内容与注册表当前提供的内容一致。对于来自公共仓库的 GitHub PR 自动开启;如果你的安全模型需要更慢的安装速度,值得永久开启。

npm#

使用 min-release-age 配置(相对,以天为单位)或 before(绝对日期)。在 .npmrc 中设置:

🌐 Use the min-release-age config (relative, in days) or before (absolute date). Set in .npmrc:

1
min-release-age=7

或者每条命令:

🌐 Or per-command:

1
npm install --min-release-age=7

如果你当前的 npm 版本还没有 min-release-age,就退而求其次,用私有镜像或者一个调用 npm view <pkg>@<version> time.<version> 的 CI 门户,并拒绝安装那些在最近 N 天内发布了最新版本的软件包。

圆面包 #

🌐 Bun

使用 --minimum-release-age 标志(秒),或在 bunfig.toml 中设置一次:

🌐 Use the --minimum-release-age flag (seconds), or set it once in bunfig.toml:

1
[install]
2
minimumReleaseAge = 604800 # 7 days, in seconds
3
minimumReleaseAgeExcludes = ["@types/node", "typescript"] # trusted bypass

或者每条命令:

🌐 Or per-command:

1
bun add @supabase/supabase-js --minimum-release-age 604800

Bun 的年龄限制只会影响新的版本——bun.lock 中已有的条目不会改变。它还会运行一个稳定性检查:如果在你的限制之外短时间内发布了多个版本,Bun 会扩展过滤器跳过那些(可能不稳定的)版本,选择一个更老、更成熟的版本。精确版本的请求(pkg@1.1.1)会遵守年龄限制,但会绕过稳定性扩展。

🌐 Bun's age gate only affects new resolutions — existing entries in bun.lock are unchanged. It also runs a stability check: if multiple versions were published close together outside your gate, Bun extends the filter to skip those (likely unstable) versions and picks an older, more mature one. Exact-version requests (pkg@1.1.1) respect the gate but bypass the stability extension.

关于基于 Deno 的 Edge Functions,请参阅下面的 Edge Functions 详细说明 部分。

🌐 For Deno-based Edge Functions, see the Edge Functions specifics section below.

核实封装来源 #

🌐 Verify package provenance

@supabase/supabase-js@supabase/auth-js@supabase/postgrest-js@supabase/realtime-js@supabase/storage-js@supabase/functions-js 通过 npm OIDC 受信任发布使用 sigstore 来源证明 进行发布。这些证明以密码学方式将每个发布的 tar 包与其构建所用的工作流运行、提交和仓库关联起来。

一个有效的 Supabase 证明总是会指向 supabase GitHub 组织 下的一个仓库。如果 npm audit signatures 报告一个已验证的证明指向其他地方的 @supabase/* 包,要把这当作一个警告信号。

🌐 A valid Supabase attestation will always resolve to a repository under the supabase GitHub organisation. If npm audit signatures reports a verified attestation pointing anywhere else for an @supabase/* package, treat that as a red flag.

安装后进行验证:

🌐 To verify after install:

1
npm audit signatures

示例输出:

🌐 Sample output:

1
audited 1 package in 0s
2
1 package has a verified registry signature

这里的失败是一个强烈的信号,表明你的注册表镜像可能被篡改,或者 tar 包在发布后被修改了。使用最新的 npm CLI(Node.js 自带的版本可能比较旧);通过 npm install -g npm@latest 安装最新版本。

🌐 A failure here is a strong signal that either your registry mirror is tampered with or the tarball was modified after publish. Use a recent npm CLI (the version bundled with Node.js can lag); install the latest with npm install -g npm@latest.

有关更多示例,请参见 supabase-js 自述文件中的 验证来源证明

🌐 See Verifying provenance attestations in the supabase-js README for additional examples.

控制生命周期脚本 #

🌐 Control lifecycle scripts

preinstallpostinstallprepare 脚本是被攻陷的依赖中最常见的代码执行入口点。

pnpm: 在 pnpm-workspace.yaml 中声明一个允许列表:

1
allowBuilds:
2
esbuild: false
3
simple-git-hooks: true

默认拒绝是目标。只有在你确实需要它们的构建运行时才添加软件包。

🌐 Default-deny is the goal. Add packages only when you genuinely need their build to run.

yarn: enableScripts: false 是默认的 —— 除非你在 package.json 里通过 dependenciesMeta 为每个包选择,否则第三方包的 postinstall 脚本不会运行。工作区仍然会运行它们自己的脚本。

npm / bun:使用 --ignore-scripts 安装,并且只为真正需要的包启用脚本。

@supabase/* 核心包不运行安装/安装后脚本。 你可以放心地把它们保留在拒绝列表中。

阻止外来依赖引用 #

🌐 Block exotic dependency references

一个可传递依赖解析到非注册源——例如 optionalDependencies: { "some-helper": "github:attacker/repo#<sha>" }——会直接从 git 对象存储或任意 URL 拉取代码,完全绕过 npm 注册表的签名、来源和隔离保证。阻止这一类引用:

pnpm:在 pnpm-workspace.yaml 中:

1
blockExoticSubdeps: true

npmallow-gitallow-remoteallow-fileallow-directory 设置每个都可以配置为 "all"(默认)、"none""root""root" 的意思是“只有当它在你自己的 package.json 中声明时才允许这种引用,绝不允许作为传递依赖”——这正是你想要的信任边界:

1
allow-git=root
2
allow-remote=root
3
allow-file=root
4
allow-directory=root

yarn: 使用 approvedGitRepositories 来允许特定的 git 来源。任何不匹配的都会被拒绝:

1
approvedGitRepositories:
2
- 'https://github.com/yarnpkg/*'
3
- 'ssh://git@github.com/yarnpkg/*'

bun:今天没有本地对应——检查你的 bun.lock 是否有非注册表引用。

固定你的包管理器 #

🌐 Pin your package manager

本地开发和持续集成之间的差异是一个悄悄的风险来源。用 sha512 哈希固定包管理器本身:

🌐 Drift between local dev and CI is a quiet source of risk. Pin the package manager itself with a sha512 hash:

1
// package.json
2
"packageManager": "pnpm@10.0.0+sha512.<hash>"

Corepack(随现代 Node 一起打包)和 pnpm/action-setup@v6+ 都会自动读取这个字段。如果一个被篡改的 npm 镜像提供了被篡改的 pnpm 二进制文件,会在运行前因为哈希检查失败而阻止执行。

🌐 Corepack (bundled with modern Node) and pnpm/action-setup@v6+ both read this field automatically. A compromised npm mirror serving a tampered pnpm binary fails the hash check instead of running.

清理未使用的依赖 #

🌐 Prune unused dependencies

每一个你不需要的依赖,都是你不需要的攻击面。两个简单的习惯:

🌐 Every dependency you don't need is attack surface you don't need. Two cheap habits:

  • 定期运行 npx depcheck(或你使用环境的等效工具),然后移除没有被任何地方导入的依赖。
  • 当出现 CVE 时,看看你的直接依赖。这个依赖做的事情你自己 20 行代码就能做吗?一些最容易被利用的包其实都是微小工具,但却成为了传递性的坑。

这是供应链防御中不那么光鲜的一面:封装越少,攻击者的机会就越少。

🌐 This is the unglamorous half of supply-chain defence: fewer packages, fewer attackers' chances.

CI / 锁文件管理 #

🌐 CI / lockfile hygiene

  • 在每个 CI 任务中运行 --frozen-lockfile / npm ci。绝不要让 CI 静默地重新生成锁文件。
  • 在 PR 中查看锁文件的差异,就像查看代码差异一样。意外的新传递依赖或版本跳跃值得提问一下。
  • 配置 Dependabot 或 Renovate 批量更新,并遵循你在本地设置的相同最小年龄。Renovate 的 minimumReleaseAge 选项是直接等价的。
  • npm audit signatures 作为非阻塞的 CI 步骤运行,这样可以早点发现被篡改的 tar 包。

保持了解 #

🌐 Stay informed

预防只是事情的一半——你还需要弄清楚上游哪里出了问题,最好是在消息传开之前就发现。

🌐 Prevention is only half the job — you also need to find out when something has gone wrong upstream, ideally before the news goes wide.

  • 在每个有锁文件的仓库中启用 Dependabot 警报。公共和私有仓库都免费。它会将你的锁文件与 GitHub 咨询数据库进行比对,并在有传递依赖变成已知漏洞版本时提醒你。
  • 订阅 GitHub Advisory Database 的 RSS feed(按生态系统筛选:npm),以便随时了解新的安全通告——即使是你没有直接依赖的包,也很有用。
  • 按计划运行 npm audit / pnpm audit,作为一个_非阻塞_的 CI 任务。把它当作一个通知器,而不是门控(审核会很吵,而且阻塞门控会让人习惯忽略它)。
  • 第三方扫描器 — Socket、Snyk、Aikido 以及类似服务通常比 GHSA feed 更快发现安全漏洞。我们不推荐具体某一个;如果你们组织已经有许可证,就直接接入。如果没有,可以根据过去事件的检测速度来评估,而不是功能列表。
  • 关注你的同龄人关注的通道。 在过去的安全事件中,上游的 GitHub 问题和少数安全研究者的社交媒体账号,往往在正式公告发布前几个小时就提供了最权威的信号。几条精心挑选的关注账号是无法替代的。

边缘函数的细节 #

🌐 Edge Functions specifics

如果你在 Supabase Edge Function 中使用 Deno 的 @supabase/supabase-js(或任何 npm: 指定符),你在运行时层无法使用 npm 端的 minimum-release-age 阈值。你可以改用以下方法:

🌐 If you're using @supabase/supabase-js (or any npm: specifier) from Deno in a Supabase Edge Function, you don't have the npm-side minimum-release-age gate available at the runtime layer. What you can do instead:

  • 在你的导入映射 / deno.json固定到具体版本——避免使用像 latest 这样的浮动标签。
  • 供应商关键依赖deno vendor)并提交供应的输出。这会将依赖锁定在一个已知的良好快照上,并完全移除运行时获取。
  • 在 CI 中使用 --lock--lock-write 来让任何引入意外内容的构建失败。
  • 保持关注 Deno — 新版本正在增加更多供应链功能(锁文件完整性、npm: 来源验证)。查看 Deno 发布说明

如果你的安全策略依赖于只有新版本 Deno 才有的功能,跟 Supabase Functions 团队聊聊吧。

🌐 Talk to the Supabase Functions team if your security posture depends on a feature only in a newer Deno.

如果你怀疑自己安装了被破坏的版本 #

🌐 If you suspect you installed a compromised version

请立即行动。操作顺序:

🌐 Act promptly. Order of operations:

  1. 把安装主机当作可能被攻破。 安装时运行的用户能读的任何东西——环境变量、文件、内存中的机密——都应该假设已经被窃取。
  2. 轮换该主机可访问的凭证:云服务提供商密钥(AWS、GCP、Azure)、Kubernetes / Vault 令牌、GitHub 令牌、npm 令牌、SSH 密钥,以及任何接触过该主机的 Supabase 服务角色密钥或匿名密钥。
  3. 清除 node_modules 以及你的包管理器缓存(npm cache clean --forcepnpm store pruneyarn cache clean)。
  4. 固定到已知良好的版本package.json 中,并在干净的缓存上重新安装。
  5. 检查 npm auditGitHub Advisory Database 了解该包。
  6. 举报它:在上游仓库提交 GitHub 安全公告,如果该版本仍然可以安装,请发送邮件给 security@npmjs.com

Supabase 在它那边做的事 #

🌐 What Supabase does on its side

  • OIDC 受信任发布。 没有长期存在的 NPM_TOKEN 密钥。每次发布都会使用绑定到发布工作流的短期 OIDC 令牌进行 npm 身份验证。
  • 来源证明。 每个 @supabase/supabase-js 以及其兄弟包的发布版本都会附带一个 sigstore 证明,将 tarball 与其源代码提交和工作流运行绑定。可以用 npm audit signatures 来验证。
  • 在六个核心包(auth-jspostgrest-jsrealtime-jsstorage-jsfunctions-jssupabase-js)中没有 postinstall / preinstall 脚本。你可以放心使用 --ignore-scripts 安装。
  • 固定版本的单仓库发布。 所有包都会一起发布相同的版本,所以当你固定其中一个时,实际上就是固定了全部。
  • 通过 GitHub 环境进行多步骤发布审批。 Stable 从 master 运行并发布,这个运行在受保护的 GitHub 环境中,发布作业在访问 npm OIDC 凭证之前,需要维护者的明确批准。

如果发现已发布的 @supabase/* 包有问题,请通过 Supabase 安全政策 举报。

🌐 If something looks wrong with a published @supabase/* package, report it via the Supabase security policy.

参考资料 #

🌐 References