Skip to content

Commit e8636d4

Browse files
committed
docs(repo): safeguard against supply chain attacks
1 parent 1761a62 commit e8636d4

3 files changed

Lines changed: 301 additions & 1 deletion

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ Testing varies per package. See the top-level [TESTING.md](docs/TESTING.md) for
145145
- **[Contributing](./CONTRIBUTING.md)** - Development guidelines
146146
- **[Release Workflows](./docs/RELEASE.md)** - Release and publishing process
147147
- **[Migration Guide](./docs/MIGRATION.md)** - Migrating to the monorepo structure
148-
- **[Security Policy](./docs/SECURITY.md)** - Security guidelines and reporting
148+
- **[Security Policy](./docs/SECURITY.md)** - Vulnerability reporting and disclosure policy
149+
- **[Securing your npm installs](./docs/NPM_PACKAGE_SECURITY.md)** - Consumer-side guide to defending your install against npm supply-chain attacks
149150

150151
## 🔐 Verifying provenance attestations
151152

@@ -172,6 +173,8 @@ audited 1 package in 0s
172173

173174
Because provenance attestations are a new capability, security features may evolve over time. Ensure you are using the latest npm CLI to verify attestation signatures reliably. This may require updating npm beyond the version bundled with Node.js.
174175

176+
For a broader checklist — minimum release age, lockfile hygiene, blocking exotic transitive deps, lifecycle script controls, and what to do if you suspect a compromise — see [Securing your npm installs](./docs/NPM_PACKAGE_SECURITY.md).
177+
175178
## 📄 License
176179

177180
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.

docs/NPM_PACKAGE_SECURITY.md

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# Securing your npm installs
2+
3+
A practical guide for consumers of `@supabase/supabase-js` (and any other npm package) to defend against npm supply-chain attacks.
4+
5+
> Looking to **report** a vulnerability in Supabase itself? See [SECURITY.md](./SECURITY.md) instead. This guide is about hardening your install of Supabase (and other) packages on your machines and in your CI.
6+
7+
## Why this guide exists
8+
9+
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.
10+
11+
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.
12+
13+
## TL;DR — what to do today
14+
15+
1. **Commit your lockfile** and install with `--frozen-lockfile` (pnpm/yarn) or `npm ci` (npm) in CI.
16+
2. **Quarantine new versions.** Set a minimum release age (≥ 7 days) so installs won't pick up a brand-new version while attackers are still in their detection window. npm, pnpm, yarn, and bun all support this natively now.
17+
3. **Block exotic transitive deps.** Refuse `github:`, `git+`, and `file:` refs that didn't come from the npm registry.
18+
4. **Verify provenance.** Run `npm audit signatures` after install. Supabase packages publish with sigstore attestations.
19+
5. **Constrain lifecycle scripts.** Default-deny `postinstall` / `preinstall` / `prepare`; allow per-package.
20+
6. **Pin a single source of truth for your package manager** (`packageManager` field in `package.json` with a sha512 hash).
21+
7. **Have a rollback plan.** Know which packages, which versions, and which credentials to rotate if a compromise is announced upstream.
22+
23+
The rest of this document expands on each of these and covers the Edge Functions / Deno case separately.
24+
25+
## Pin your dependency versions
26+
27+
A committed lockfile is the floor, not the ceiling.
28+
29+
**Application repos**:
30+
31+
- Commit `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, or `bun.lock`.
32+
- In CI, install with `npm ci` / `pnpm install --frozen-lockfile` / `yarn install --immutable` / `bun install --frozen-lockfile`. These fail if `package.json` and the lockfile disagree, which is what you want.
33+
- Caret ranges (`^1.2.3`) in `package.json` are fine **if** you also have a lockfile and the rest of the guidance below — the lockfile is what actually gets installed.
34+
35+
**Pin transitive risk with overrides.** If you don't trust a particular transitive dep version, force a known-good version via:
36+
37+
```jsonc
38+
// npm and pnpm
39+
"overrides": {
40+
"some-dep": "1.2.3"
41+
}
42+
43+
// yarn
44+
"resolutions": {
45+
"some-dep": "1.2.3"
46+
}
47+
```
48+
49+
This is the lever to reach for when you see a CVE on a transitive you don't directly depend on.
50+
51+
## Quarantine new versions (minimum release age)
52+
53+
Most npm compromises are detected and remediated within hours. A short quarantine on freshly-published versions is the single highest-leverage setting.
54+
55+
### pnpm (recommended)
56+
57+
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):
58+
59+
```yaml
60+
minimumReleaseAge: 10080 # 7 days, in minutes
61+
minimumReleaseAgeExclude:
62+
- '@your-org/*' # bypass for your own internal packages
63+
```
64+
65+
Set `minimumReleaseAge: 0` only if you have a specific reason to opt out of the default.
66+
67+
#### `trustPolicy` (pnpm)
68+
69+
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:
70+
71+
```yaml
72+
trustPolicy: no-downgrade
73+
trustPolicyExclude:
74+
- 'some-package' # opt specific packages out if needed
75+
trustPolicyIgnoreAfter: '180d' # ignore checks for packages older than 180 days
76+
```
77+
78+
### yarn (Berry / v4+)
79+
80+
In `.yarnrc.yml`:
81+
82+
```yaml
83+
npmMinimalAgeGate: '7d'
84+
npmPreapprovedPackages: # opt specific packages out of all package gates
85+
- '@your-org/*'
86+
```
87+
88+
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 just installed could vanish, breaking your build, if you don't wait it out.
89+
90+
Two related yarn settings worth knowing about while you're in `.yarnrc.yml`:
91+
92+
- **`enableScripts: false`** is the default in yarn — postinstall scripts from third-party packages don't run. Workspaces still run their own.
93+
- **`enableHardenedMode: true`** makes yarn re-query remote registries to confirm that the lockfile content matches what the registry currently serves. Auto-on for GitHub PRs from public repos; worth turning on permanently if your threat model warrants slower installs.
94+
95+
### npm
96+
97+
Use the [`min-release-age`](https://docs.npmjs.com/cli/configuring-npm/config#min-release-age) config (relative, in days) or [`before`](https://docs.npmjs.com/cli/configuring-npm/config#before) (absolute date). Set in `.npmrc`:
98+
99+
```ini
100+
min-release-age=7
101+
```
102+
103+
Or per-command:
104+
105+
```bash
106+
npm install --min-release-age=7
107+
```
108+
109+
If `min-release-age` isn't available in your npm version yet, fall back to a private mirror or to a CI gate that calls `npm view <pkg>@<version> time.<version>` and rejects installs whose newest version was published in the last N days.
110+
111+
### Bun
112+
113+
Use the `--minimum-release-age` flag (seconds), or set it once in `bunfig.toml`:
114+
115+
```toml
116+
[install]
117+
minimumReleaseAge = 604800 # 7 days, in seconds
118+
minimumReleaseAgeExcludes = ["@types/node", "typescript"] # trusted bypass
119+
```
120+
121+
Or per-command:
122+
123+
```bash
124+
bun add @supabase/supabase-js --minimum-release-age 604800
125+
```
126+
127+
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 just 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.
128+
129+
### Deno / Edge Functions
130+
131+
Deno has an unstable `--minimum-dependency-age` flag that accepts minutes, an ISO-8601 duration, or an absolute RFC3339 cutoff:
132+
133+
```bash
134+
deno install --minimum-dependency-age=P7D # 7-day quarantine
135+
deno install --minimum-dependency-age=10080 # 7 days, in minutes
136+
deno install --minimum-dependency-age=2026-04-01 # cut off at a fixed date
137+
deno install --minimum-dependency-age=0 # disable
138+
```
139+
140+
Track Deno's release notes for when this stabilises. See the [Edge Functions](#edge-functions-specifics) section below for the Supabase-runtime-specific picture.
141+
142+
## Use a private registry or mirror
143+
144+
A proxy in front of `registry.npmjs.org` is the strongest layer of defense your org can add. It lets you enforce quarantine, audit, block, and roll back independently of what npmjs.org does.
145+
146+
Common options (we don't endorse any one): [Verdaccio](https://verdaccio.org/), JFrog Artifactory, Sonatype Nexus, AWS CodeArtifact, GitHub Packages.
147+
148+
A minimal `.npmrc` pointing to a private registry:
149+
150+
```ini
151+
registry=https://npm.your-org.example/
152+
@supabase:registry=https://npm.your-org.example/
153+
//npm.your-org.example/:_authToken=${NPM_TOKEN}
154+
always-auth=true
155+
```
156+
157+
Notes:
158+
159+
- Never commit the token. Use `${NPM_TOKEN}` from your CI environment (npm 7+ expands env vars in `.npmrc`).
160+
- Scoped registry lines let you point specific scopes at a different upstream.
161+
- For a per-project `.npmrc`, put it next to `package.json`. For a per-user one, `~/.npmrc`.
162+
163+
## Verify package provenance
164+
165+
`@supabase/supabase-js`, `@supabase/auth-js`, `@supabase/postgrest-js`, `@supabase/realtime-js`, `@supabase/storage-js`, and `@supabase/functions-js` publish with [sigstore provenance attestations](https://docs.npmjs.com/generating-provenance-statements) via npm OIDC trusted publishing. The attestations cryptographically tie each published tarball to the workflow run, commit, and repository it was built from.
166+
167+
To verify after install:
168+
169+
```bash
170+
npm audit signatures
171+
```
172+
173+
Sample output:
174+
175+
```text
176+
audited 1 package in 0s
177+
1 package has a verified registry signature
178+
```
179+
180+
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`.
181+
182+
See the existing [README — Verifying provenance attestations](../README.md#-verifying-provenance-attestations) section for additional examples.
183+
184+
## Control lifecycle scripts
185+
186+
`preinstall`, `postinstall`, and `prepare` scripts are the single most common code-execution entry point in a compromised dep.
187+
188+
**pnpm**: declare an allowlist in `pnpm-workspace.yaml`:
189+
190+
```yaml
191+
allowBuilds:
192+
esbuild: false
193+
simple-git-hooks: true
194+
```
195+
196+
Default-deny is the goal. Add packages only when you genuinely need their build to run.
197+
198+
**yarn**: `enableScripts: false` is the default — postinstall scripts from third-party packages don't run unless you opt in per package via `dependenciesMeta` in `package.json`. Workspaces still run their own scripts.
199+
200+
**npm / bun**: install with `--ignore-scripts` and only enable scripts for the packages that truly need them.
201+
202+
**The `@supabase/*` core packages run no install/postinstall scripts.** You can safely keep them on the deny list.
203+
204+
## Block exotic dependency references
205+
206+
The TanStack/router attack used `optionalDependencies: { "@tanstack/setup": "github:tanstack/router#<sha>" }` to pull payload code from a fork's git object store, bypassing the npm registry entirely. Block this class of ref:
207+
208+
**pnpm**: in `pnpm-workspace.yaml`:
209+
210+
```yaml
211+
blockExoticSubdeps: true
212+
```
213+
214+
**npm**: the `allow-git`, `allow-remote`, `allow-file`, and `allow-directory` settings each take `"all"` (default), `"none"`, or `"root"`. `"root"` means "only allow this kind of reference if it's declared in your own `package.json`, never as a transitive dep" — which is exactly the trust boundary you want:
215+
216+
```ini
217+
allow-git=root
218+
allow-remote=root
219+
allow-file=root
220+
allow-directory=root
221+
```
222+
223+
**yarn**: use `approvedGitRepositories` to allowlist specific git sources. Anything not matching is rejected:
224+
225+
```yaml
226+
approvedGitRepositories:
227+
- 'https://github.com/yarnpkg/*'
228+
- 'ssh://git@github.com/yarnpkg/*'
229+
```
230+
231+
**bun**: no native equivalent today — inspect your `bun.lock` for non-registry refs.
232+
233+
## Pin your package manager
234+
235+
Drift between local dev and CI is a quiet source of risk. Pin the package manager itself with a sha512 hash:
236+
237+
```jsonc
238+
// package.json
239+
"packageManager": "pnpm@10.0.0+sha512.<hash>"
240+
```
241+
242+
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.
243+
244+
## CI / lockfile hygiene
245+
246+
- Run `--frozen-lockfile` / `npm ci` in every CI job. Never let CI silently regenerate the lockfile.
247+
- Review lockfile diffs in PRs the same way you review code diffs. Unexpected new transitives or version jumps deserve a question.
248+
- Configure Dependabot or Renovate to batch updates and respect the same min-age you set locally. Renovate's `minimumReleaseAge` option is the direct equivalent.
249+
- Run `npm audit signatures` as a non-blocking CI step so a tampered tarball is caught early.
250+
251+
## Edge Functions specifics
252+
253+
If you're using `@supabase/supabase-js` (or any `npm:` specifier) from Deno in a Supabase Edge Function, the picture is slightly different.
254+
255+
Deno's `--minimum-dependency-age` (see above) is currently behind an unstable flag, and the Supabase Edge Runtime upgrade path for Deno is being worked on separately. Until your runtime supports it, the practical defenses are:
256+
257+
- **Pin to exact versions** in your import map / `deno.json` — avoid floating tags like `latest`.
258+
- **Vendor critical dependencies** (`deno vendor`) and commit the vendored output. This freezes the dep at a known-good snapshot and removes the runtime fetch entirely.
259+
- **Use `--lock` and `--lock-write`** in CI to fail any build that pulls in unexpected content.
260+
- **Stay current on Deno** — newer versions are landing more supply-chain features (lockfile integrity, `npm:` provenance verification, min-age stabilisation). Track the [Deno release notes](https://github.com/denoland/deno/releases).
261+
262+
Talk to the Supabase Functions team if your security posture depends on a feature only in a newer Deno.
263+
264+
## If you suspect you installed a compromised version
265+
266+
Move quickly. Order of operations:
267+
268+
1. **Treat the install host as potentially compromised.** Anything readable by the user that ran the install — env vars, files, secrets in memory — should be assumed exfiltrated.
269+
2. **Rotate credentials reachable from that host**: cloud provider keys (AWS, GCP, Azure), Kubernetes / Vault tokens, GitHub tokens, npm tokens, SSH keys, and any Supabase service-role keys or anon keys that touched the box.
270+
3. **Wipe `node_modules`** and your package manager cache (`npm cache clean --force`, `pnpm store prune`, `yarn cache clean`).
271+
4. **Pin to a known-good version** in `package.json` and reinstall against a fresh cache.
272+
5. **Check `npm audit` and the [GitHub Advisory Database](https://github.com/advisories)** for the package.
273+
6. **Report it**: file a GitHub Security Advisory on the upstream repo, and email `security@npmjs.com` if the version is still installable.
274+
275+
## What Supabase does on its side
276+
277+
You shouldn't have to trust us — that's the whole point of this guide — but for completeness:
278+
279+
- **OIDC trusted publishing.** No long-lived `NPM_TOKEN` secret. Each publish is authenticated against npm using a short-lived OIDC token bound to the release workflow.
280+
- **Provenance attestations.** Every release of `@supabase/supabase-js` and its sibling packages ships with a sigstore attestation tying the tarball to its source commit and workflow run. Verify with `npm audit signatures`.
281+
- **No `postinstall` / `preinstall` scripts** in any of the six core packages (`auth-js`, `postgrest-js`, `realtime-js`, `storage-js`, `functions-js`, `supabase-js`). You can safely install with `--ignore-scripts`.
282+
- **Fixed-version monorepo releases.** All packages release together with identical versions, so when you pin one, you pin them all.
283+
284+
If something looks wrong with a published `@supabase/*` package, report it via the channels in [SECURITY.md](./SECURITY.md).
285+
286+
## References
287+
288+
- TanStack/router compromise postmortem: [TanStack/router#7383](https://github.com/TanStack/router/issues/7383), [GHSA-g7cv-rxg3-hmpx](https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx).
289+
- Adnan Khan, ["The Monsters in Your Build Cache — GitHub Actions Cache Poisoning"](https://adnanthekhan.com/2024/05/06/the-monsters-in-your-build-cache-github-actions-cache-poisoning/) (May 2024).
290+
- GitHub Security Lab, ["Keeping your GitHub Actions and workflows secure: Preventing pwn requests"](https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/) (Part 1 of a 4-part series, 2021–2025).
291+
- npm trusted publishing: <https://docs.npmjs.com/trusted-publishers>.
292+
- npm provenance: <https://docs.npmjs.com/generating-provenance-statements>.
293+
- pnpm `minimumReleaseAge`, `blockExoticSubdeps`, `allowBuilds`: <https://pnpm.io/settings>.
294+
- Renovate `minimumReleaseAge`: <https://docs.renovatebot.com/configuration-options/#minimumreleaseage>.
295+
- GitHub Advisory Database: <https://github.com/advisories>.

docs/SECURITY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Contact: https://hackerone.com/supabase
22
Canonical: https://supabase.com/.well-known/security.txt
33

4+
> Looking to **harden your install** of Supabase npm packages against supply-chain attacks (version pinning, minimum release age, private registries, provenance verification)? See [Securing your npm installs](./NPM_PACKAGE_SECURITY.md). The rest of this page is about reporting vulnerabilities in Supabase itself.
5+
46
At Supabase, we consider the security of our systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
57

68
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our clients and our systems.

0 commit comments

Comments
 (0)