Files
supabase/apps/docs/content/guides/security/npm-security.mdx
Katerina Skroumpelou c9d34a60ef docs: add guide for securing npm installs against supply-chain attacks (#46384)
Adds a consumer-side guide for hardening npm installs of @supabase/*
packages: lockfile hygiene, minimum-release-age quarantine across
pnpm/yarn/npm/bun, private registries, provenance verification,
lifecycle script controls, blocking exotic transitive refs, Edge
Functions specifics, and an incident-response checklist.
  
Lives at guides/security/ alongside platform-security and
product-security; filesystem-driven sidebar picks it up automatically.

Originally drafted in supabase/supabase-js#2382 and moved here per
review.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Added an npm security hardening guide (lockfile & CI practices,
release-age quarantines, transitive/exotic dependency controls,
provenance verification, lifecycle-script hardening, package-manager
pinning, CI/lockfile hygiene, Deno/Edge guidance, incident-response
checklist).
  * Added a navigation entry for the new guide.
  * Note: guide frontmatter contains unresolved merge-conflict markers.
* **Chores**
* Expanded spelling allowlist to include common tech terms (e.g.,
lockfile, sigstore, postinstall).

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46384?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Chris Chinchilla <chris.ward@supabase.io>
2026-05-26 17:20:12 +03:00

299 lines
18 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: 'npm-security'
title: 'Securing npm installs'
description: 'Consumer-side guide to hardening your npm installs of Supabase packages against supply-chain attacks.'
---
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 just Supabase's.
<Admonition type="tip">
Looking to **report** a vulnerability in Supabase itself? See the [Supabase security policy](https://github.com/supabase/supabase-js/security) instead. This guide is about hardening your install of Supabase (and other) packages on your machines and in your CI.
</Admonition>
## Why this guide exists
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. **Commit your lockfile** and install with `--frozen-lockfile` (pnpm/yarn) or `npm ci` (npm) in CI.
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.
3. **Block exotic transitive dependencies.** Refuse `github:`, `git+`, and `file:` refs that didn't come from the npm registry.
4. **Verify provenance.** Run `npm audit signatures` after install. Supabase packages publish with [sigstore attestations](https://www.sigstore.dev/) (cryptographic proof tying each tarball to the workflow run, commit, and repo it was built from).
5. **Constrain lifecycle scripts.** Default-deny `postinstall` / `preinstall` / `prepare`; allow per-package.
6. **Pin a single source of truth for your package manager** (`packageManager` field in `package.json` with a sha512 hash).
7. **Have a rollback plan.** Know which packages, which versions, and which credentials to rotate if a compromise is announced upstream.
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.
**Application repos**:
- Commit `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, or `bun.lock`.
- 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.
- 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.
**Pin transitive risk with overrides.** If you don't trust a particular transitive dep version, force a known-good version via:
```jsonc
// npm and pnpm
"overrides": {
"some-dep": "1.2.3"
}
// yarn
"resolutions": {
"some-dep": "1.2.3"
}
```
This is the lever to reach for when you see a CVE on a transitive you don't directly depend on.
### Beware `npx` / `pnpm dlx` / `bunx`
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:
- **Pin the version**: `npx pkg@1.2.3` instead of `npx pkg@latest`.
- **Move the tool into `devDependencies`** so it's covered by your lockfile and the rest of this guide, then invoke it through `npm exec` / `pnpm exec` / `yarn run`.
Treat any ad-hoc registry fetch the same way you'd treat `curl … | bash`.
## Quarantine new versions (minimum release age)
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 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):
```yaml
minimumReleaseAge: 10080 # 7 days, in minutes
minimumReleaseAgeExclude:
- '@your-org/*' # bypass for your own internal packages
```
Set `minimumReleaseAge: 0` only if you have a specific reason to opt out of the default.
#### `trustPolicy` (pnpm)
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:
```yaml
trustPolicy: no-downgrade
trustPolicyExclude:
- 'some-package' # opt specific packages out if needed
trustPolicyIgnoreAfter: '180d' # ignore checks for packages older than 180 days
```
### `yarn` (berry / v4+)
In `.yarnrc.yml`:
```yaml
npmMinimalAgeGate: '7d'
npmPreapprovedPackages: # opt specific packages out of all package gates
- '@your-org/*'
```
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.
Two related yarn settings worth knowing about while you're in `.yarnrc.yml`:
- **`enableScripts: false`** is the default in yarn — postinstall scripts from third-party packages don't run. Workspaces still run their own.
- **`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.
### `npm`
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`:
```ini
min-release-age=7
```
Or per-command:
```bash
npm install --min-release-age=7
```
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.
### Bun
Use the `--minimum-release-age` flag (seconds), or set it once in `bunfig.toml`:
```toml
[install]
minimumReleaseAge = 604800 # 7 days, in seconds
minimumReleaseAgeExcludes = ["@types/node", "typescript"] # trusted bypass
```
Or per-command:
```bash
bun add @supabase/supabase-js --minimum-release-age 604800
```
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.
For Deno-based Edge Functions, see the [Edge Functions specifics](#edge-functions-specifics) section below.
## Verify package provenance
`@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.
A valid Supabase attestation will always resolve to a repository under the [`supabase` GitHub organisation](https://github.com/supabase). 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:
```bash
npm audit signatures
```
Sample output:
```text
audited 1 package in 0s
1 package has a verified registry signature
```
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`.
See [Verifying provenance attestations](https://github.com/supabase/supabase-js#-verifying-provenance-attestations) in the `supabase-js` README for additional examples.
## Control lifecycle scripts
`preinstall`, `postinstall`, and `prepare` scripts are the single most common code-execution entry point in a compromised dep.
**pnpm**: declare an allowlist in `pnpm-workspace.yaml`:
```yaml
allowBuilds:
esbuild: false
simple-git-hooks: true
```
Default-deny is the goal. Add packages only when you genuinely need their build to run.
**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.
**npm / bun**: install with `--ignore-scripts` and only enable scripts for the packages that truly need them.
**The `@supabase/*` core packages run no install/postinstall scripts.** You can safely keep them on the deny list.
## Block exotic dependency references
A transitive dependency that resolves to a non-registry source — for example `optionalDependencies: { "some-helper": "github:attacker/repo#<sha>" }` — pulls code directly from a git object store or arbitrary URL, bypassing the npm registry's signing, provenance, and quarantine guarantees entirely. Block this class of ref:
**pnpm**: in `pnpm-workspace.yaml`:
```yaml
blockExoticSubdeps: true
```
**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:
```ini
allow-git=root
allow-remote=root
allow-file=root
allow-directory=root
```
**yarn**: use `approvedGitRepositories` to allowlist specific git sources. Anything not matching is rejected:
```yaml
approvedGitRepositories:
- 'https://github.com/yarnpkg/*'
- 'ssh://git@github.com/yarnpkg/*'
```
**bun**: no native equivalent today — inspect your `bun.lock` for non-registry refs.
## Pin your package manager
Drift between local dev and CI is a quiet source of risk. Pin the package manager itself with a sha512 hash:
```jsonc
// package.json
"packageManager": "pnpm@10.0.0+sha512.<hash>"
```
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 actually need is attack surface you don't actually need. Two cheap habits:
- Periodically run `npx depcheck` (or the equivalent for your stack) and remove dependencies that aren't imported anywhere.
- Look at your direct dependencies when a CVE lands. Is the dep doing something you could do in 20 lines yourself? Some of the most-exploited packages are tiny utilities that became transitive footguns.
This is the unglamorous half of supply-chain defence: fewer packages, fewer attackers' chances.
## CI / lockfile hygiene
- Run `--frozen-lockfile` / `npm ci` in every CI job. Never let CI silently regenerate the lockfile.
- Review lockfile diffs in PRs the same way you review code diffs. Unexpected new transitive dependencies or version jumps deserve a question.
- Configure Dependabot or Renovate to batch updates and respect the same min-age you set locally. Renovate's `minimumReleaseAge` option is the direct equivalent.
- Run `npm audit signatures` as a non-blocking CI step so a tampered tarball is caught early.
## 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.
- **Enable [Dependabot alerts](https://docs.github.com/en/code-security/dependabot/dependabot-alerts/about-dependabot-alerts)** on every repository that has a lockfile. Free for both public and private repos. It checks your lockfile against the GitHub Advisory Database and pings you when a transitive becomes a known-vulnerable version.
- **Subscribe to the [GitHub Advisory Database](https://github.com/advisories)** RSS feed (filter by ecosystem: npm) for ambient awareness of new advisories — useful even for packages you don't depend on directly.
- **Run `npm audit` / `pnpm audit` on a schedule** as a _non-blocking_ CI job. Treat it as a notifier, not a gate (audit is noisy and a blocking gate trains people to ignore it).
- **Third-party scanners** — Socket, Snyk, Aikido, and similar services often spot compromises faster than the GHSA feed. We don't endorse a specific one; if your org already has a license, plug it in. If not, evaluate based on detection time on past incidents, not feature lists.
- **Watch the channels your peers watch.** During past compromises, the upstream GitHub issue and a handful of security-researcher accounts on social media were the canonical signal hours before formal advisories landed. There's no substitute for a few well-curated follows.
## Edge Functions specifics
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:
- **Pin to exact versions** in your import map / `deno.json` — avoid floating tags like `latest`.
- **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.
- **Use `--lock` and `--lock-write`** in CI to fail any build that pulls in unexpected content.
- **Stay current on Deno** — newer versions are landing more supply-chain features (lockfile integrity, `npm:` provenance verification). Track the [Deno release notes](https://github.com/denoland/deno/releases).
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
Move quickly. Order of operations:
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 stolen.
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.
3. **Wipe `node_modules`** and your package manager cache (`npm cache clean --force`, `pnpm store prune`, `yarn cache clean`).
4. **Pin to a known-good version** in `package.json` and reinstall against a fresh cache.
5. **Check `npm audit` and the [GitHub Advisory Database](https://github.com/advisories)** for the package.
6. **Report it**: file a GitHub Security Advisory on the upstream repo, and email `security@npmjs.com` if the version can still be installed.
## What Supabase does on its side
- **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.
- **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`.
- **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`.
- **Fixed-version monorepo releases.** All packages release together with identical versions, so when you pin one, you pin them all.
- **Multi-step release approval via GitHub environments.** Stable publishes from `master` run inside a protected GitHub environment that requires explicit approval from a maintainer before the publish job can access npm OIDC credentials.
If something looks wrong with a published `@supabase/*` package, report it via the [Supabase security policy](https://github.com/supabase/supabase-js/security).
## References
- 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).
- ["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).
- 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, 20212025).
- npm trusted publishing: [docs.npmjs.com/trusted-publishers](https://docs.npmjs.com/trusted-publishers).
- npm provenance: [docs.npmjs.com/generating-provenance-statements](https://docs.npmjs.com/generating-provenance-statements).
- pnpm `minimumReleaseAge`, `blockExoticSubdeps`, `allowBuilds`: [pnpm.io/settings](https://pnpm.io/settings).
- Renovate `minimumReleaseAge`: [docs.renovatebot.com](https://docs.renovatebot.com/configuration-options/#minimumreleaseage).
- GitHub Advisory Database: [github.com/advisories](https://github.com/advisories).