Dependency Pinning Best Practices

Permalink to "Dependency Pinning Best Practices"

This workflow is part of Supply Chain Auditing & Dependency Verification. Without exact version and hash pinning, every package install is a potential substitution attack: a compromised maintainer account, a typosquatted package, or a silent CDN cache swap can silently replace a trusted artifact with a malicious one. Semantic version ranges compound the problem by authorising the package manager to select a different tarball on every fresh install, making builds non-reproducible and audit trails meaningless.

The pages that follow build on this foundation — Pinning Transitive Dependencies in Monorepos covers workspace-level resolution strategies for enterprise codebases, and Lockfile Mapping & Analysis shows how to extract and validate the full resolved dependency graph for security review.


Dependency pinning and SRI verification pipeline A left-to-right flow diagram showing four stages: Package Manifest (exact version), Lockfile with SHA-512 hashes, CI frozen-install gate, and Browser SRI hash check. Arrows connect each stage; a red rejection path exits the CI gate when hashes mismatch. Package Manifest [email protected] (exact version) Lockfile resolved + sha512-… (committed to repo) CI Gate npm ci / pnpm --frozen-lockfile Browser SRI sha384-… integrity check Hash mismatch → build fails 1. Author declares 2. Resolver records 3. CI verifies 4. Browser enforces

Prerequisites

Permalink to "Prerequisites"

Before starting this workflow, confirm the following:


Conceptual Foundation: Version Pinning vs Hash Pinning

Permalink to "Conceptual Foundation: Version Pinning vs Hash Pinning"

Semantic versioning ranges (^1.6.0, ~1.6.0) instruct the resolver to select the newest satisfying release on every fresh install. This means two developers running npm install a week apart may receive different tarballs — and different code — for the same declared dependency.

Exact version pinning ("axios": "1.6.2") eliminates this ambiguity, but it still permits registry substitution: a registry under an attacker’s control could serve an entirely different tarball under version 1.6.2. Hash pinning closes this gap by recording a cryptographic digest of the tarball bytes in the lockfile. The package manager refuses to install any tarball that does not produce the same digest, making substitution attacks cryptographically infeasible.

The W3C Subresource Integrity specification (see Understanding Cryptographic Hash Algorithms) extends this same guarantee to browser asset delivery: the integrity attribute on <script> and <link> tags embeds a SHA-384 digest that browsers verify before executing any fetched resource.


Step 1 — Pin Direct Dependencies and Commit Lockfiles

Permalink to "Step 1 — Pin Direct Dependencies and Commit Lockfiles"

Remove all caret and tilde operators from package.json. For each ecosystem, the lock command is:

npm

# Remove ranges, then regenerate the lockfile with integrity fields
npm install --package-lock-only --save-exact

pnpm

pnpm install --shamefully-hoist=false
# pnpm-lock.yaml always records exact resolved versions and SHA-512 digests

pip (Python build tools in the pipeline)

pip-compile --generate-hashes --output-file requirements.txt requirements.in

After running these commands, package-lock.json contains entries like:

"node_modules/axios": {
  "version": "1.6.2",
  "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
  "integrity": "sha512-7eSe1fMDnBJkMcGiNWzuCXpSKnN9A1wKlrPuNI0QO71qPHHPCaGlAqEBBonDxlrg==",
  "license": "MIT"
}

The integrity field is a SHA-512 digest encoded in Subresource Integrity format. Commit this file. It is the ground truth your CI pipeline will enforce.

Verification signal: git diff package-lock.json should show only version bumps and new integrity fields, not resolved-version churn across unrelated packages.


Step 2 — Configure CI to Install from Frozen Lockfiles

Permalink to "Step 2 — Configure CI to Install from Frozen Lockfiles"

Installing with npm install in CI re-resolves the dependency tree against the live registry and can silently update lockfile entries. Replace all install commands with their frozen equivalents:

GitHub Actions (npm)

- name: Install dependencies
  run: npm ci
  # npm ci reads package-lock.json exactly; fails if lockfile is absent or stale

GitHub Actions (pnpm)

- name: Install dependencies
  run: pnpm install --frozen-lockfile
  # Exits non-zero if pnpm-lock.yaml is missing or would be modified

GitHub Actions (pip)

- name: Install Python dependencies
  run: pip install --require-hashes -r requirements.txt
  # Rejects any package whose tarball SHA-256/SHA-384 does not match the lockfile entry

GitLab CI (npm)

install:
  stage: build
  script:
    - npm ci --ignore-scripts
    # --ignore-scripts prevents lifecycle hooks from running arbitrary code during install

--ignore-scripts deserves special attention: npm lifecycle hooks (preinstall, postinstall) run arbitrary shell commands with full network access. Disabling them during CI reduces the risk of a compromised package executing a payload at install time. Review any packages that require postinstall scripts before re-enabling them selectively.

Verification signal: CI log lines added N packages from N contributors (npm) or Lockfile is up to date, resolution step is skipped (pnpm) confirm frozen install. Any updated or removed message indicates lockfile drift and should fail the pipeline.


Step 3 — Inject SRI Hashes into Frontend Bundles

Permalink to "Step 3 — Inject SRI Hashes into Frontend Bundles"

Browser-executed scripts and stylesheets loaded from a CDN or asset server receive no protection from a backend lockfile. Subresource Integrity closes this gap by requiring browsers to verify the hash of every fetched resource before execution. Configure your build tool to emit integrity attributes automatically.

Webpack 5 with webpack-subresource-integrity

// webpack.config.js
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');

module.exports = {
  output: {
    crossOriginLoading: 'anonymous',
  },
  plugins: [
    new SubresourceIntegrityPlugin({
      hashFuncNames: ['sha384'],
      enabled: process.env.NODE_ENV === 'production',
    }),
  ],
};

The plugin computes a SHA-384 digest of each emitted chunk and writes it into the integrity attribute of the <script> and <link> tags in the generated HTML:

<script
  src="/static/vendor.abc123.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux6KsrOlS6gZT7m42WkRUOKbM1jLer7"
  crossorigin="anonymous"
></script>

The crossorigin="anonymous" attribute is mandatory. Without it the browser sends a non-CORS request, and the SRI check is skipped entirely for cross-origin resources — see Browser Enforcement & Security Boundaries for the full fetch lifecycle explanation.

Vite 4+ (built-in)

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        // Vite injects integrity automatically when crossOriginLoading is set
      },
    },
  },
  experimental: {
    renderBuiltUrl(filename) {
      return { relative: true };
    },
  },
};

For Vite, use the community plugin vite-plugin-subresource-integrity until native support stabilises in Vite 5.

Verification signal: Inspect the generated index.html — every <script src> and <link rel="stylesheet"> for production assets must carry an integrity attribute starting with sha384- and a crossorigin="anonymous" attribute.


Step 4 — Automate Hash Rotation on Dependency Upgrades

Permalink to "Step 4 — Automate Hash Rotation on Dependency Upgrades"

Pinning creates a maintenance obligation: when a dependency is upgraded, the lockfile hash and the SRI hash in the HTML output must both be regenerated. Failing to rotate them leaves stale hashes that either break the build or, worse, silently pass if a developer commits a manually patched hash without running a full build.

Use Dependabot or Renovate Bot to open upgrade PRs that include both the manifest bump and the lockfile regeneration:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    versioning-strategy: increase-if-necessary
    commit-message:
      prefix: "chore(deps)"

In the CI pipeline for dependency-update PRs, add a step that confirms the HTML output contains fresh integrity attributes:

# Verify that no integrity attribute is older than the current build
grep -oP 'integrity="sha384-[^"]*"' dist/index.html | sort -u > current_hashes.txt
grep -oP 'integrity="sha384-[^"]*"' dist/index.html.bak | sort -u > previous_hashes.txt
if diff -q current_hashes.txt previous_hashes.txt > /dev/null; then
  echo "WARNING: SRI hashes unchanged after dependency update — verify the build ran"
  exit 1
fi

Configuration Reference

Permalink to "Configuration Reference"
Option Ecosystem Value Security Implication
Exact version ("1.6.2") npm / pnpm No ^ or ~ prefix Prevents automatic minor/patch upgrades that bypass QA
npm ci npm Replaces npm install in CI Fails if lockfile is absent or would be modified; does not write to lockfile
pnpm install --frozen-lockfile pnpm Equivalent to npm ci Exits non-zero if pnpm-lock.yaml would change
yarn install --immutable Yarn Berry Equivalent to npm ci Fails if yarn.lock is stale
pip install --require-hashes pip Requires --hash entries in requirements Rejects packages missing a hash entry; blocks silent substitution
--ignore-scripts npm Disables lifecycle hooks Prevents postinstall from executing arbitrary code during CI install
hashFuncNames: ['sha384'] webpack-subresource-integrity Algorithm selection SHA-384 is the W3C-recommended default; SHA-512 is acceptable but produces longer attributes
crossorigin="anonymous" HTML Required alongside integrity Without this attribute, browsers skip SRI verification for cross-origin resources
--save-exact npm Writes exact versions to package.json Sets "axios": "1.6.2" instead of "axios": "^1.6.2"

Integration with Adjacent Tooling

Permalink to "Integration with Adjacent Tooling"

Dependency pinning is the entry point for the broader verification pipeline:

  1. Lockfile → SBOM. Once every dependency is pinned and hash-verified, Automated SBOM Generation can produce a precise software bill of materials. The SBOM is only as accurate as the resolution it describes — pinning guarantees determinism.

  2. Lockfile → Vulnerability scanning. Tools like Trivy, Grype, and npm audit operate against the resolved dependency tree. A pinned lockfile gives scanners an exact version to look up in CVE databases; a range-resolved tree produces ambiguous results.

  3. SBOM → Provenance attestation. After SBOM generation, Provenance Verification Workflows attaches SLSA-level provenance records to build artifacts, creating a chain of custody from source commit to deployed asset.

  4. SRI hashes → CSP enforcement. SRI hashes can be combined with a require-sri-for directive via Configuring Content-Security-Policy with SRI to instruct browsers to reject any script or stylesheet loaded without a verified integrity attribute.


Troubleshooting

Permalink to "Troubleshooting"

npm ci fails with Missing: lockfile Root cause: package-lock.json is listed in .gitignore or was never committed. Fix: Remove the lockfile from .gitignore, run npm install locally to generate it, and commit the result. Establish a branch protection rule that rejects PRs which delete the lockfile.

npm ci fails with npm ERR! cipm can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync Root cause: A developer ran npm install locally after editing package.json without committing the updated lockfile. Fix: Run npm install locally to regenerate the lockfile, stage package-lock.json, and push before re-running CI.

pip install --require-hashes fails with THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE Root cause: A package was upgraded upstream without regenerating requirements.txt via pip-compile --generate-hashes. Fix: Run pip-compile --generate-hashes --upgrade requirements.in and commit the new requirements.txt.

Browser console: Failed to find a valid digest in the 'integrity' attribute Root cause: The SRI hash in the HTML does not match the served file. Common causes: CDN edge cache serving a stale asset after a deploy, or a build tool emitting the hash before finalising minification. Fix: Invalidate the CDN cache for the affected asset path. Ensure your build pipeline computes the SRI hash after all transformations (minification, tree-shaking) complete.

Browser console: Subresource Integrity: The resource ... has an integrity attribute, but the resource requires the request to be CORS enabled Root cause: crossorigin="anonymous" is missing from the <script> or <link> tag. Fix: Add crossorigin="anonymous" to every tag that carries an integrity attribute. This is the most common real-world SRI deployment error.

pnpm install --frozen-lockfile fails after adding a new workspace package Root cause: Workspace packages added to pnpm-workspace.yaml without running pnpm install first to update pnpm-lock.yaml. Fix: Run pnpm install locally (without --frozen-lockfile) after workspace changes, commit the updated lockfile, then switch CI back to --frozen-lockfile.


Security Boundary

Permalink to "Security Boundary"

Dependency pinning with hash verification protects against: registry substitution (different tarball served at the same version), silent version drift (resolver picking a newer release), and lockfile tampering detected at CI time.

It does not protect against: a malicious package that was already in the lockfile at the time of initial install, a compromised build tool or package manager binary, vulnerabilities present in pinned versions (address via Vulnerability Tracking & Triage), or runtime code injection after the asset has been delivered to the browser (address with a strict Content Security Policy). Pinning guarantees reproducibility and tamper-evidence — it is not a substitute for vulnerability management.


Permalink to "Related"

Articles in This Topic

Pinning Transitive Dependencies in Monorepos
npm ci vs pnpm --frozen-lockfile vs yarn --immutable
Back to Supply Chain Auditing & Dependency Verification