An Express API named api-service can pass tests, build an OCI image, and still ship code nobody intended to approve. A developer adds expresss instead of express to package.json; the public registry resolves the near-name package, the GitHub Actions runner installs it, and the release image contains it. The attacker did not need repository write access, a stolen deploy key, or access to the production cluster. The defender’s question is narrower and more useful: how do we prevent unreviewed code from entering a build through dependency resolution? We will follow api-service from its manifest through its CI build and into its published image.
The build path crosses several trust boundaries before the image reaches deployment. A public package registry supplies code based on a name in a manifest, the CI runner executes installation, and the artifact registry stores the resulting image for later deployment. A malicious package can seek code execution in the build, introduce a harmful transitive dependency, or steal a publishing token available to the runner. The following path makes those boundaries visible:
This diagram shows why dependency selection is a supply-chain decision rather than a local coding detail. Once the runner fetches a package, its contents become part of the build input and may execute installation hooks unless the pipeline restricts them.
The manifest is the first boundary because npm install treats each declared package name as an instruction to resolve and fetch code. In api-service, express at version 4.18.3 is expected, while the invented name expresss at version 1.0.0 is a typo that points to a different package. This attack is typosquatting, where an attacker publishes a package whose name resembles a trusted dependency and relies on a human error to select it.
A lockfile records the resolved package version, registry metadata, and integrity digest for a particular dependency graph. That record makes later installs reproducible only after the incorrect package has already been accepted and committed. The review failure is therefore visible in two places: package.json contains an unexpected direct dependency, and package-lock.json contains a new resolved entry and integrity value. What package name and lockfile entry should make the expresss addition fail review?
This code shows that a successful installation is not evidence that the intended dependency was selected. The installer resolved expresss exactly as requested, then recorded the result in the lockfile; deterministic repetition of that mistake is still a mistake. Reviewing dependency diffs means treating an unfamiliar name, unexpected version, or newly added integrity entry as code entering the release.
Review rule A lockfile proves what the installer selected. It does not prove that the selection was authorized or safe.
A misspelled public name starts in the manifest, but a private package can be replaced even when its name is correct. Dependency confusion occurs when a build resolves a package from an unintended registry, commonly because a public package with the same name or a higher matching version wins over an internal package. The attacker needs the ability to publish to a registry the build consults, not access to the private source repository.
For api-service, an internal dependency such as @acme/auth-client must always resolve from the organization’s private registry. A broad version range permits future versions that satisfy the declared range, while registry precedence determines where the installer looks. A lockfile narrows the result for a committed build, but it cannot correct a registry mapping that points a rebuild at the wrong source, nor can it protect a deliberate lockfile update that reviewers fail to inspect. The control target is deterministic resolution from the intended registry for each package scope.
The comparison separates the boundary each threat crosses from the evidence that reveals it: