Dependency Pinning and Lockfile Integrity: Preventing Supply Chain Attacks in CI
Problem
Dependency confusion and typosquatting attacks exploit the gap between “I declared a dependency” and “I verified the dependency I got.” Version pinning alone is insufficient, a compromised registry can serve different code for the same version number. Lockfiles with integrity hashes are the first line of defence: they pin the exact content hash of every dependency, not just its version string.
Threat Model
- Adversary: Supply chain attacker who compromises a package registry, publishes a malicious package with the same name as your internal package (dependency confusion), or publishes a typosquat package similar to a popular dependency.
- Blast radius: Every build that installs the compromised dependency. The malicious code runs during install (npm postinstall scripts, setup.py execution) with full pipeline permissions.
Configuration
npm: Hash-Pinned Lockfile
# ALWAYS use npm ci (not npm install) in CI.
# npm ci installs from package-lock.json exactly - fails if lockfile is out of sync.
npm ci
# Verify lockfile integrity:
# package-lock.json contains SHA-512 integrity hashes for every package.
# Example entry:
# "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
# "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
# npm ci verifies these hashes. If the content doesn't match: the install fails.
# Configure private registry for scoped packages (prevent dependency confusion):
# .npmrc
@your-org:registry=https://npm.your-company.com/
//npm.your-company.com/:_authToken=${NPM_TOKEN}
# CRITICAL: claim your org scope on npmjs.com even if you use a private registry.
# This prevents attackers from publishing @your-org/package-name on the public registry.
Python: Hash-Pinned Requirements
# Generate requirements with hashes:
pip-compile --generate-hashes requirements.in > requirements.txt
# Example output in requirements.txt:
# flask==3.0.3 \
# --hash=sha256:34e815e5029... \
# --hash=sha256:5f1c7b...
# Install with hash verification:
pip install --require-hashes -r requirements.txt
# If any package content doesn't match its hash: install fails.
# CI workflow:
- name: Install dependencies
run: pip install --require-hashes -r requirements.txt
# Fails if: lockfile modified, hash mismatch, or package content changed on PyPI
Go: Module Verification
# Go modules use go.sum for hash verification.
# go.sum contains cryptographic hashes for all module contents.
# Verify all modules:
go mod verify
# Expected: all modules verified
# In CI, set GONOSUMCHECK only for private modules:
GONOSUMCHECK=github.com/your-org/*
# Enable the checksum database for all other modules:
GONOSUMDB=github.com/your-org/*
GOPROXY=https://proxy.golang.org,direct
# The Go checksum database (sum.golang.org) provides a tamper-proof
# record of expected module hashes.
CI Lockfile Verification
# .github/workflows/lockfile-check.yml
# Verify lockfile is committed and matches package declarations.
name: Lockfile Integrity
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Verify npm lockfile
run: |
npm ci
git diff --exit-code package-lock.json
# Fails if npm ci modified the lockfile (meaning it was out of sync)
- name: Check for new dependencies
run: |
# Alert on new dependencies added in this PR
ADDED=$(git diff origin/main -- package-lock.json | grep '^\+.*"resolved"' | wc -l)
if [ "$ADDED" -gt 0 ]; then
echo "::warning::$ADDED new dependencies added in this PR, review required"
fi
Preventing Dependency Confusion
# Claim your organisation's namespace on public registries
# even if you only use private packages.
# npm: create the scope on npmjs.com
npm init --scope=@your-org
# Publish a placeholder: npm publish --access public
# PyPI: register your package name
# Create a minimal setup.py and publish a placeholder to pypi.org
# This prevents attackers from registering @your-org/secret-package
# or your-org-secret-package on the public registry.
Expected Behaviour
npm ci/pip install --require-hashes/go mod verifyall pass in CI- Any hash mismatch fails the build immediately
- New dependencies in PRs are flagged for review
- Private package scopes claimed on public registries
- Lockfiles committed to Git and reviewed in PRs
Trade-offs
| Control | Impact | Risk | Mitigation |
|---|---|---|---|
npm ci (not npm install) |
Fails if lockfile is out of sync | Developers must update lockfile locally before pushing | Add pre-commit hook that runs npm install and checks for lockfile changes. |
| Hash verification | Catches content tampering | Build fails if registry serves different content (even for legitimate re-publishes) | Rare in practice. If it happens: verify with the package maintainer. |
| Private registry scope | Prevents dependency confusion | Must maintain the private registry | Use a cloud-hosted private registry (GitHub Packages, npm Enterprise, Artifactory). |
| Claiming public namespace | Prevents namespace squatting | Must maintain the placeholder package | Publish a “this is a placeholder” README. No code needed. |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Lockfile out of sync | npm ci fails with lockfile mismatch |
CI build failure at install step | Developer runs npm install locally, commits updated lockfile. |
| Hash mismatch (registry compromise) | pip install --require-hashes fails with hash error |
CI build failure; hash verification error in log | Investigate: is this a registry issue or legitimate package update? Verify with the package maintainer. Pin to known-good version. |
| Dependency confusion attack | Malicious package installed instead of internal package | Unexpected behaviour in builds; new package name in lockfile diff | Configure private registry for all internal scopes. Claim namespaces on public registries. |
When to Consider a Managed Alternative
Manual lockfile review does not scale past 10 repositories.
- Snyk (#48): Dependency vulnerability scanning and monitoring across all repositories. Automatic PR for vulnerable dependency updates.
- Socket (#102): Behavioural analysis of dependencies (detects malicious behaviour, not just known CVEs). Catches supply chain attacks that Snyk/Trivy miss.
- Phylum (#101): Automated malicious package detection using static and dynamic analysis.
Premium content pack: Dependency security configurations. .npmrc templates for private registry, pip-compile CI workflow, Go module verification scripts, lockfile CI check workflows, and namespace claiming guides for npm/PyPI/Go.