Dependency confusion is a supply chain issue that affects how package managers choose where to download a dependency from. If your build or developer tooling can see both a private package registry and a public registry, an attacker may try to publish a package with the same name as an internal one and rely on the resolver to pick the public version. In the worst case, that can lead to malicious code being installed during a build, test, or release process.
For UK SMEs, the risk is not limited to large engineering teams. Smaller organisations often move quickly, use a mix of hosted CI services and developer laptops, and rely on package managers such as npm and PyPI to keep delivery moving. That combination can create a narrow but important gap between what teams think will be installed and what is actually fetched. This article focuses on practical controls that reduce that gap without turning delivery into a manual process.
Key takeaways
- Use explicit registry scoping and reserved namespaces so internal npm and PyPI packages cannot be confused with public ones.
- Treat CI runners as trusted assets and restrict their outbound access to approved package sources only.
- Commit lockfiles or pinned dependency manifests and review any change to package source, scope, or registry mapping.
- Separate build and publish permissions so ordinary pipeline jobs cannot release or alter package configuration.
What dependency confusion is and why it matters
Package managers resolve dependencies according to configured registries, package names, version constraints, and local cache state. If a package name exists in more than one place, the resolver may prefer the public registry unless the private source is explicitly scoped or pinned. That is the core of dependency confusion: the build system trusts the wrong source because the naming and registry rules are not strict enough.
npm and PyPI are common targets because they sit at the centre of many modern build pipelines. npm projects often have a large number of transitive dependencies, and PyPI-based Python services are frequently built in automated environments that pull packages from the internet as part of the build. If internal package names are predictable, or if private registries are not tightly configured, the attack surface is created by the delivery process itself rather than by a vulnerable application feature.
This is different from general dependency risk and transitive dependency abuse. General dependency risk includes outdated libraries, known vulnerabilities, and abandoned packages. Transitive dependency abuse is about a trusted dependency bringing in something risky further down the chain. Dependency confusion is more specific: the issue is package source selection, not just package quality. That distinction matters because the control set is different. You need registry governance, namespace discipline, and pipeline restrictions, not only vulnerability scanning. For a broader view of third-party software exposure, see how third-party software introduces cyber risk for UK SMEs.
Where the risk enters the software delivery process
The risk can enter at several points. Developer workstations are one common source because local npm or pip configuration may differ from the build environment. A developer may have a private registry configured correctly, while the CI runner falls back to the public registry or inherits a default from a base image. That inconsistency is enough to create an exposure.
Build systems and CI runners are especially important because they are trusted execution points. They often have access to source code, secrets, signing keys, and deployment credentials. If a malicious package is installed during build, the impact can extend beyond the application artefact to token theft, environment inspection, or tampering with release outputs. This is why dependency confusion is a pipeline integrity issue, not only a package management issue.
Namespace collisions are another common pattern. Teams sometimes create internal packages with names that are generic, short, or similar to public packages. Examples include names that describe a function rather than a domain, such as util, common, shared, or internal-tools. If the private package is not scoped or namespaced, the chance of collision rises over time as public ecosystems grow.
Organisations also increase exposure when they allow multiple package sources without a clear precedence model. A build that can query both a private registry and the public internet, with no explicit source mapping, is difficult to reason about. The same is true when teams rely on ad hoc instructions in README files rather than enforced configuration in the repository and pipeline. Good practice is to make the intended source part of the codebase, not tribal knowledge.
Preventive controls for npm and PyPI environments
The first control is explicit registry scoping. In npm, that usually means using scoped packages for internal modules and mapping the scope to a private registry in .npmrc. For example, an internal scope such as @company can be directed to a private registry, while public packages continue to come from the default registry. The key point is that internal names should not be left unscoped if you can avoid it.
A typical pattern is to keep repository-level configuration under version control and avoid relying on user-level settings. For npm, that may include entries such as registry=https://registry.npmjs.org/ for public packages and @company:registry=https://npm.company.example/ for private ones, with authentication tokens stored as CI secrets or injected at runtime. For Python, the equivalent is to define trusted package indexes explicitly, for example through pip configuration, requirements files, or build tooling that points to a private index first and only falls back where that is intentionally allowed.
Package naming conventions matter as much as technical configuration. Internal packages should follow a namespace standard that is unlikely to overlap with public ecosystems. In practice, that means using a company prefix, a reserved scope, or a naming convention that is documented and enforced. If you already have internal packages with generic names, treat renaming as a controlled change rather than a cosmetic one. It reduces ambiguity for both people and tooling.
Lockfiles and pinned versions are also important. A lockfile records the exact dependency tree that was resolved at build time, which reduces the chance of unexpected source changes. In npm, package-lock.json or an equivalent lock mechanism should be committed and validated in CI. In Python, the same principle applies through pinned requirements, hash checking, or a dependency management tool that supports reproducible installs. Lockfiles do not stop dependency confusion on their own, but they make source drift easier to detect and review.
Controlled dependency updates are the final part of the prevention layer. New dependencies should be added through a reviewable process, not directly from a developer workstation into a release branch. If a package name changes, or if a new internal package is introduced, the change should be reviewed for naming collisions, registry mapping, and ownership. This is a good place to align with secure development practices already used for code review and release approval, as described in embedding security into CI/CD pipelines without slowing teams.
Hardening CI/CD pipelines against package substitution
CI/CD hardening should assume that package registries are untrusted unless explicitly allowed. The simplest improvement is to use authenticated private registries with least-privilege tokens. Build jobs should receive only the credentials needed to fetch approved packages, and those credentials should be scoped to read-only access where possible. Publishing credentials should never be reused for ordinary builds.
Restricting outbound network access from build jobs is one of the most effective controls. If a build runner can reach the public internet freely, it can often reach package registries freely as well. A more defensive pattern is to allow egress only to the specific package endpoints required for the job. That can be implemented with firewall rules, proxy allow-lists, or network policies in containerised build environments. The objective is not to block all internet access indiscriminately, but to make package retrieval deterministic.
Separating build, test, and publish permissions reduces the blast radius of a compromised job. A job that compiles code should not be able to publish packages. A job that publishes packages should not be able to modify dependency configuration. Where possible, use distinct service accounts or workload identities for each stage. In GitHub Actions, GitLab CI, Azure DevOps, or similar platforms, this usually means limiting token scopes, disabling unnecessary permissions, and avoiding long-lived secrets in repository variables.
It is also worth reviewing how runners are provisioned. Ephemeral runners are preferable to shared long-lived machines because they reduce the chance of cached credentials, stale package metadata, or cross-job contamination. If you must use persistent runners, clear package caches between jobs and treat the runner as a sensitive asset. This is especially important where build logs, artefacts, or caches may contain package metadata that could reveal internal naming patterns.
Detection and monitoring for suspicious dependency activity
Prevention should be paired with detection. One useful signal is an unexpected package source during install. If a build that should only use a private registry suddenly resolves a package from a public endpoint, that is worth investigating. The same applies if a package version appears that was not part of the normal release process. Build logs, package manager verbose output, and proxy logs can all help identify this drift.
For npm, monitor for changes in .npmrc, package-lock.json, and install commands that bypass the expected registry settings. For Python, watch for changes in pip configuration, alternate index URLs, or direct URL installs that do not match the approved pattern. In both ecosystems, the presence of a new dependency source should be treated as a configuration change, not just a routine install event.
Software composition analysis can help by comparing the dependency inventory in the build against the approved baseline. If a package appears that is not in the expected bill of materials, or if a package name matches an internal naming convention but comes from a public registry, that is a useful anomaly. This is where SBOMs and dependency inventories become operationally valuable, because they let you compare what was intended with what was actually built. For a deeper implementation view, see implementing software supply chain assurance controls for technical teams.
Detection should also include review of package publishing activity. If your organisation publishes internal packages, alert on new package names, unusual maintainers, changes to registry destinations, and unexpected publish times. A package that appears outside the normal release window, or from an unfamiliar identity, should trigger a manual check. Where available, use repository and registry audit logs to tie package events back to the actor, pipeline, and commit.
Secure publishing and release governance
Publishing controls are often overlooked because teams focus on consuming dependencies rather than producing them. However, internal package namespaces should be protected before release, not after. If you know a package name will be used in future, reserve it early in the private registry and document the owner. That prevents accidental reuse and reduces the chance that a public package with the same name is later introduced into the workflow.
Approvals for new dependencies and package name changes should be lightweight but explicit. A sensible control is to require a review when a package is added to the allow-list, when a registry mapping changes, or when a package is moved between scopes. This does not need to become a bureaucratic gate. It simply ensures that the people responsible for build integrity are aware of changes that affect source trust.
Release checks should include package provenance and integrity. Provenance means evidence of where the package came from and how it was built. Integrity means confidence that the artefact has not been altered since it was produced. In practical terms, that may involve checksums, signed artefacts, trusted build pipelines, or provenance metadata generated by the build system. These controls do not eliminate dependency confusion, but they make it easier to prove what was built and where it came from.
For teams already working towards broader software assurance, dependency confusion controls fit naturally alongside secure development lifecycle checkpoints, release approvals, and supplier assurance. They are part of the same discipline: knowing which code you trust, why you trust it, and how that trust is enforced in tooling.
Practical implementation checklist for technical teams
If you want a short starting point, begin with the following changes in order of impact.
First, make internal package names unambiguous. Use a reserved npm scope or a clear Python package namespace, and document it as a standard. Second, configure registry sources explicitly in the repository and CI environment, not just on individual laptops. Third, commit and enforce lockfiles or pinned dependency manifests so that builds are reproducible. Fourth, restrict build runner egress to approved package endpoints. Fifth, split build and publish permissions so that ordinary jobs cannot release packages.
After that, improve observability. Capture package resolution logs, registry access logs, and dependency inventory data. Compare those records against the expected source of truth. If you already use a SIEM, forward registry and pipeline audit events into it so that unusual package activity can be correlated with commits, runner identities, and release jobs. This is a practical way to extend existing monitoring rather than creating a separate process.
Finally, define ownership. Someone should be accountable for package namespace governance, registry configuration, and exceptions. If a team needs to use a non-standard registry or a new internal package naming pattern, there should be a documented approval path. Exceptions are sometimes necessary, but they should be visible and time-bound.
How this maps to wider software supply chain controls
Dependency confusion sits within a broader set of software supply chain controls that includes SBOMs, provenance, build integrity, and supplier assurance. An SBOM tells you what was included in a build. Provenance tells you how it was produced. Build integrity tells you whether the artefact has remained trustworthy since production. Dependency confusion prevention strengthens all three by reducing the chance that an unapproved package enters the build in the first place.
Frameworks such as OWASP SAMM, the NIST Secure Software Development Framework, and MITRE ATT&CK can help structure the work. OWASP SAMM is useful for maturity planning across governance and implementation. The NIST SSDF is helpful for turning secure development into repeatable practice, especially around provenance, dependency management, and build protection. MITRE ATT&CK can be used to model how a compromised package might support initial access, credential theft, or persistence within the delivery environment. If you want to connect this topic to attack modelling, supply chain attack modelling using MITRE ATT&CK for technical practitioners is a useful companion read.
For UK SMEs, the practical aim is not perfect elimination of risk. It is to make registry selection explicit, reduce ambiguity in package naming, and ensure that CI/CD systems only trust the sources they are meant to trust. That gives you a much stronger position without adding unnecessary friction to delivery.
If you are reviewing your software supply chain controls and want help turning these patterns into a working plan, speak to a consultant.
Frequently asked questions
How do I stop dependency confusion in npm and PyPI?
Use explicit private registry configuration, reserved internal namespaces, committed lockfiles or pinned requirements, and CI egress controls so builds can only fetch packages from approved sources. Pair that with review for new dependencies and changes to package naming or registry mappings.
What is the difference between dependency confusion and dependency hijacking?
Dependency confusion is about a package manager choosing the wrong registry or source because of naming and precedence rules. Dependency hijacking is broader and usually refers to taking over a package, account, or distribution path that was already trusted. Both are supply chain problems, but the controls are not identical.


Comments are closed