CoursesSoftware supply chain securitySecuring source & dependencies

Dependency integrity

Pinning, lockfiles, and dependency confusion.

Advanced14 min · lesson 5 of 18

You inherit the security of every dependency you pull in, so dependency integrity is about controlling exactly what enters your build and proving it did not change. The first tool is the lockfile — package-lock.json, go.sum, Cargo.lock — which records the exact version and a cryptographic hash of every dependency, direct and transitive. Commit it, and every build (and every developer) resolves to the identical, verified bytes instead of "whatever the latest matching version is today."

terminal
# the lockfile pins exact versions AND hashes of the whole tree
$ npm ci # installs strictly from package-lock.json, fails if it drifts
$ go mod verify # checks every module against the hashes in go.sum
# never "npm install" in CI (it can update the lockfile); always the locked/verified install

Pin, and pin the base image too

Pinning extends beyond app dependencies. Pin your container base image by digest (FROM image@sha256:...), not a floating tag, so a rebuild uses the exact bytes you reviewed and cannot silently pull a re-pushed or tampered base. Pin CI action/plugin versions the same way — a GitHub Action referenced by a mutable tag can change under you. The principle is uniform: mutable references are trust holes; pin everything that goes into the build to immutable identifiers.

Dockerfile / .github
FROM gcr.io/distroless/static@sha256:d71f4f2a9c... # base pinned by digest
# GitHub Actions pinned by full commit SHA, not a tag:
# uses: actions/checkout@8f4b7f8... # not @v4 (a mutable tag)

Defend against dependency confusion

Internal package names are a specific risk: if your build can fall back to a public registry, an attacker publishing a public package with your internal name and a higher version can hijack the resolution. Two defenses: namespace internal packages under a scope you own (@acme/...) so a public name cannot collide, and configure the resolver to fetch internal scopes only from your private registry, with no public fallback. Never let a build silently reach the public internet for a package that should be internal.

.npmrc
@acme:registry=https://registry.acme.internal/ # internal scope → private only
# and crucially: do not configure a public fallback for @acme/*
# a request for @acme/auth can therefore never resolve to a public impostor
A lockfile you do not enforce is decoration
Committing a lockfile only helps if CI actually installs from it strictly — npm ci, not npm install; the locked/verified install for your ecosystem, not the loose one. The loose install can update the lockfile or resolve new versions, defeating the pin. Enforce the locked install in CI, verify hashes where the toolchain supports it, and treat any unexpected lockfile change in a diff as a security event to review.