Supply Chain Security
Your project's security posture affects everyone downstream. A popular package is a high-value target precisely because compromising it is cheaper than compromising its users individually.
The realistic threat model
Ranked by observed frequency in actual incidents, not by drama:
- Compromised maintainer account — phishing, credential reuse, no 2FA. The single most common root cause of package compromise.
- Malicious dependency — typosquat, or a legitimate package taken over via a maintainer handoff to a stranger who asked nicely.
- Compromised CI — a workflow with excessive permissions, an unpinned third-party
action,
pull_request_targetexecuting fork code. - Malicious contribution — a subtle backdoor in a PR from a patient contributor who spent months building trust.
- Build-time injection —
postinstallscripts,setup.pyexecuting at install, compromised build servers.
Note what is not at the top: vulnerabilities in your own code. Those matter, but the supply chain is where the leverage is.
Baseline hardening
Do all of these. Together they take an afternoon and remove most of the realistic risk.
Accounts
- Hardware 2FA (WebAuthn) on GitHub and every package registry, for every maintainer. SMS 2FA is not sufficient against the phishing that actually happens.
- Registry publishing via OIDC/trusted publishing instead of long-lived tokens.
- Audit org membership and repo access quarterly. Remove inactive maintainers' publish rights, kindly and explicitly — this is not a demotion, it is hygiene.
Repository
- Branch protection on
main, including for admins. - Signed commits/tags for releases (
git config commit.gpgsign true; Sigstoregitsignavoids GPG key management entirely). - Enable private vulnerability reporting: Settings → Security → "Private vulnerability reporting". This gives researchers a channel that is not a public issue.
- Enable Dependabot alerts, secret scanning, and push protection.
CI (see also ci-pipelines)
permissions:
contents: read # default-deny at the top of every workflow
- Pin third-party actions to a full commit SHA:
Tags are mutable. A compromised action with- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1@v3reaches every repo instantly — this has happened, more than once, at scale. Use Dependabot to keep pinned SHAs fresh so pinning does not mean staleness. - Never
pull_request_target+ checkout of PR head. - No self-hosted runners on public repos unless ephemeral and isolated.
- Restrict
GITHUB_TOKENto read-only by default in org settings.
Dependencies (see dependency-hygiene)
- Commit the lockfile. Install with
npm ci/pip install -r requirements.txt --require-hashes/cargo --locked. - Disable install scripts where feasible (
npm ci --ignore-scripts). - Review any new dependency's install hooks before adding it.
SECURITY.md
# Security Policy
## Supported Versions
| Version | Supported |
|---------|-----------|
| 4.x | Yes |
| 3.x | Security fixes until 2026-12-31 |
| < 3.0 | No |
## Reporting a Vulnerability
Report privately via [GitHub Security Advisories](../../security/advisories/new)
or email security@example.org.
Please do not open a public issue for security problems.
- Acknowledgement within **48 hours**
- Assessment within **7 days**
- Fix or mitigation plan within **30 days** for high severity
- We credit reporters in the advisory unless you prefer otherwise
## Scope
In scope: the published package, the CLI, the default configuration.
Out of scope: issues requiring an already-compromised host, denial of service
via deliberately malformed input to a debug-only tool, dependencies (report upstream).
Only promise timelines you will meet. A missed 48-hour promise is what turns a cooperative researcher into a public disclosure.
Handling a report
- Acknowledge fast, even with no assessment yet.
- Reproduce, and determine severity honestly. CVSS is a rough tool; the questions that matter are: what can an attacker do, what access do they need, and how many users are affected in a default configuration?
- Open a private GitHub Security Advisory. It gives you a private fork to develop the fix, and a CVE request button.
- Fix in the private fork. Do not push the fix to a public branch before the advisory — the commit is the disclosure, and exploit development is faster than user upgrades.
- Coordinate disclosure. Standard is 90 days, or on fix release, whichever is first. Negotiate with the reporter; most are reasonable when you communicate.
- Release the fix to every supported branch, patch releases only, no other changes in them.
- Publish the advisory with affected versions, patched versions, workaround for users who cannot upgrade, and reporter credit.
- Notify downstream — GitHub advisories propagate to Dependabot automatically. Also post where your users are.
If a reporter is hostile or demands a bounty you have not offered, stay factual and keep to the process. If they publish early, do not escalate publicly; ship the fix.
Provenance and signing
For anything widely installed, make the build verifiable:
npm publish --provenance # links artifact → repo → workflow run
# PyPI: attestations via trusted publishing (pypa/gh-action-pypi-publish)
cosign sign-blob --yes dist/tool.tar.gz # Sigstore, keyless via OIDC
gh attestation verify dist/tool.tar.gz --repo owner/repo
Publish alongside a release: SHA256SUMS, signatures, and an SBOM
(syft . -o spdx-json, or cargo sbom). Also document the verification command in
your install docs — an unverifiable signature nobody knows how to check provides no
security, only the appearance of it.
OpenSSF Scorecard
# .github/workflows/scorecard.yml — weekly, publishes results to the Security tab
- uses: ossf/scorecard-action@v2
with: { results_file: results.sarif, results_format: sarif, publish_results: true }
Treat the score as a checklist, not a target. The checks that actually reduce risk:
Branch-Protection, Token-Permissions, Pinned-Dependencies, Dangerous-Workflow,
Signed-Releases, Code-Review. The ones that measure process theater more than
safety: CII-Best-Practices, Contributors, Packaging. Do not contort the project
to raise a number.
Reviewing PRs for supply-chain risk
Escalate scrutiny sharply when a PR touches:
.github/workflows/**— especiallypermissions, new actions, or new triggerspackage.jsonscripts,setup.py,build.rs,Makefileinstall targets- Any new dependency, especially one that is new, low-download, or recently transferred
- Base64 blobs, minified vendored code, unexplained binaries, or generated files committed without their generator
- Network calls in build or test code
.npmrc,.pypirc, credentials handling, anything reading environment variables
Specific tells worth knowing: obfuscated payloads assembled from string fragments, code that behaves differently under CI environment variables, a dependency version bump paired with an unrelated build script change, and a first-time contributor whose PR touches only CI configuration.
Ask about anything you do not understand. "I don't follow what this line does — can you explain?" is a complete and sufficient review comment, and a legitimate contributor will answer it happily.
Maintainer handoff
The scenario behind several major incidents: a burned-out maintainer hands publishing rights to a helpful stranger.
If you are handing off: verify identity beyond a GitHub account, transfer gradually (commit rights → review rights → publish rights), keep 2FA requirements in place, and announce the change publicly. If you are stepping away entirely and nobody has earned that trust, archive the project and say so in the README. An archived project is safe; an abandoned one with live publishing rights is a liability.
If you are asked to take over a project: expect and welcome scrutiny.
Anti-patterns
- No 2FA on the publishing account. Root cause of the majority of package compromises.
- Unpinned third-party actions.
pull_request_targetwith fork checkout.- Long-lived registry tokens when OIDC is available.
- Fixing a vulnerability in a public commit before the advisory.
- No SECURITY.md, forcing researchers to open a public issue.
- Promising a 24-hour response you cannot honor.
- Treating "it's just a dev dependency" as safe. Dev dependencies run on developer machines with SSH keys and cloud credentials. That is the target.
- Ignoring a report because the reporter was rude.
- Accepting a maintainer offer from a stranger because you are tired.