apps/docs/content/guides/security/npm-security.mdx
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.
Looking to report a vulnerability in Supabase itself? See the Supabase security policy instead. This guide is about hardening your install of Supabase (and other) packages on your machines and in your CI.
</Admonition>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.
--frozen-lockfile (pnpm/yarn) or npm ci (npm) in CI.github:, git+, and file: refs that didn't come from the npm registry.npm audit signatures after install. Supabase packages publish with sigstore attestations (cryptographic proof tying each tarball to the workflow run, commit, and repo it was built from).postinstall / preinstall / prepare; allow per-package.packageManager field in package.json with a sha512 hash).The rest of this document expands on each of these and covers the Edge Functions / Deno case separately.
A committed lockfile is the floor, not the ceiling.
Application repos:
package-lock.json, pnpm-lock.yaml, yarn.lock, or bun.lock.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.^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 gets installed.Pin transitive risk with overrides. If you don't trust a particular transitive dep version, force a known-good version via:
// 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.
npx / pnpm dlx / bunxThese 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 [email protected] instead of npx pkg@latest.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.
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):
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:
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:
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 recently 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.npmUse the min-release-age config (relative, in days) or before (absolute date). Set in .npmrc:
min-release-age=7
Or per-command:
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.
Use the --minimum-release-age flag (seconds), or set it once in bunfig.toml:
[install]
minimumReleaseAge = 604800 # 7 days, in seconds
minimumReleaseAgeExcludes = ["@types/node", "typescript"] # trusted bypass
Or per-command:
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 outside your gate, Bun extends the filter to skip those (likely unstable) versions and picks an older, more mature one. Exact-version requests ([email protected]) respect the gate but bypass the stability extension.
For Deno-based Edge Functions, see the Edge Functions specifics section below.
@supabase/supabase-js, @supabase/auth-js, @supabase/postgrest-js, @supabase/realtime-js, @supabase/storage-js, and @supabase/functions-js publish with sigstore provenance attestations 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. 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:
npm audit signatures
Sample output:
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 in the supabase-js README for additional examples.
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:
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.
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:
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:
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:
approvedGitRepositories:
- 'https://github.com/yarnpkg/*'
- 'ssh://[email protected]/yarnpkg/*'
bun: no native equivalent today — inspect your bun.lock for non-registry refs.
Drift between local dev and CI is a quiet source of risk. Pin the package manager itself with a sha512 hash:
// package.json
"packageManager": "[email protected]+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.
Every dependency you don't need is attack surface you don't need. Two cheap habits:
npx depcheck (or the equivalent for your stack) and remove dependencies that aren't imported anywhere.This is the unglamorous half of supply-chain defence: fewer packages, fewer attackers' chances.
--frozen-lockfile / npm ci in every CI job. Never let CI silently regenerate the lockfile.minimumReleaseAge option is the direct equivalent.npm audit signatures as a non-blocking CI step so a tampered tarball is caught early.Prevention is only half the job — you also need to find out when something has gone wrong upstream, ideally before the news goes wide.
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).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 — avoid floating tags like latest.deno vendor) and commit the vendored output. This freezes the dep at a known-good snapshot and removes the runtime fetch entirely.--lock and --lock-write in CI to fail any build that pulls in unexpected content.npm: provenance verification). Track the Deno release notes.Talk to the Supabase Functions team if your security posture depends on a feature only in a newer Deno.
Act promptly. Order of operations:
node_modules and your package manager cache (npm cache clean --force, pnpm store prune, yarn cache clean).package.json and reinstall against a fresh cache.npm audit and the GitHub Advisory Database for the package.[email protected] if the version can still be installed.NPM_TOKEN secret. Each publish is authenticated against npm using a short-lived OIDC token bound to the release workflow.@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.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.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.
minimumReleaseAge, blockExoticSubdeps, allowBuilds: pnpm.io/settings.minimumReleaseAge: docs.renovatebot.com.