Modern .NET applications rarely depend only on code written inside the repository. A typical enterprise application can depend on dozens or even hundreds of NuGet packages, internal libraries, SDKs, analyzers, and build tools.
That dependency model creates an important security risk: dependency confusion.
A dependency confusion attack happens when a malicious package is published to a public package source using the same package identifier as an internal package. If the build system resolves the malicious public package instead of the trusted internal package, attacker-controlled code can enter the software supply chain.
The problem is particularly dangerous because the application source code may look completely normal.
The malicious code can enter during:
Developer workstation
|
v
dotnet restore
|
v
Package resolution
|
v
Build / test / deployment
This article explains how dependency confusion can affect enterprise .NET builds and how to detect the risk using package-source controls, dependency inspection, restore diagnostics, and CI security gates.
What Is Dependency Confusion?
Imagine an organization has an internal package:
Contoso.Logging.Core
The package is hosted on a private NuGet feed.
A developer writes:
<ItemGroup>
<PackageReference Include="Contoso.Logging.Core"
Version="4.2.0" />
</ItemGroup>
The developer expects the package to come from the organization's private feed.
Now imagine an attacker publishes a package with the same ID to a public package source.
The attacker chooses a version such as:
Contoso.Logging.Core 99.0.0
If the restore configuration allows both private and public sources and package resolution is not properly controlled, the build can potentially select the unintended package.
The dangerous part is that the package reference itself does not look suspicious.
<PackageReference Include="Contoso.Logging.Core"
Version="4.2.0" />
The security problem exists in the dependency resolution environment, not necessarily in the project file.
Why Enterprise .NET Builds Are Vulnerable
Enterprise environments commonly use multiple package sources.
For example:
<packageSources>
<add key="nuget.org"
value="https://api.nuget.org/v3/index.json" />
<add key="CompanyFeed"
value="https://packages.company.local/nuget/v3/index.json" />
</packageSources>
This is convenient because the application can consume both:
Public open-source packages
Internal company packages
But the combination creates a trust-boundary problem.
The build system needs to know which package IDs are allowed to originate from which feed.
Without that distinction, a public package source can potentially compete with an internal source for the same package ID.
The First Detection Step: Inventory Package Sources
Start by identifying every package source used during restore.
Check the repository's NuGet.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="CompanyFeed"
value="https://packages.company.local/nuget/v3/index.json" />
<add key="nuget.org"
value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
The <clear /> element is important in controlled environments because it prevents unexpected package sources inherited from another configuration layer from silently participating in restore.
Remember that NuGet configuration can come from multiple locations.
A developer machine may have user-level configuration that the repository does not contain.
That means:
Repository configuration
+
User configuration
+
Machine configuration
+
CI configuration
can influence package restore.
This is one reason a build can behave differently on a developer workstation and in CI.
Detect Internal Package IDs
The next step is to identify packages that are supposed to be private.
For example:
Contoso.*
Fabrikam.*
Company.Product.*
Company.Platform.*
Do not rely only on naming conventions, however.
An organization may have internal packages that do not follow a consistent prefix.
Create an inventory containing:
| Package ID | Expected Source | Publicly Available? | Risk |
|---|---|---|---|
| Contoso.Logging.Core | Private | No | High |
| Contoso.Platform.Auth | Private | No | High |
| Newtonsoft.Json | Public | Yes | Normal |
| Company.Data.Client | Private | No | High |
The key question is:
Which package IDs must never be resolved from a public feed?
That list becomes the foundation for your detection policy.
Inspect the Resolved Dependency Graph
A project file tells you what the project requests.
It does not tell you everything about what the final application actually consumes.
Direct dependencies can bring transitive dependencies:
Application
|
+-- Package A
| |
| +-- Package B
| |
| +-- Package C
|
+-- Internal Package
Inspect the resolved dependency graph rather than looking only at direct PackageReference elements.
For SDK-style projects, dependency information can be inspected with standard .NET tooling.
For example:
dotnet list package
For newer SDK environments, the equivalent package-listing command can also be used through the modern dotnet package command family.
The important objective is to identify:
Package ID
Resolved Version
Requested Version
Direct / Transitive
and then associate that package with its expected source.
Use the Assets File During Investigation
The generated obj/project.assets.json file is particularly useful during security investigations.
It contains the resolved dependency graph produced by restore.
For example:
obj/
└── project.assets.json
You can search the file for a suspicious package ID:
grep -n "Contoso.Logging.Core" obj/project.assets.json
On Windows, a PowerShell search can be used:
Select-String `
-Path "obj/project.assets.json" `
-Pattern "Contoso.Logging.Core"
The assets file can help determine what NuGet actually resolved rather than what the developer intended.
Do not treat it as a permanent source-of-truth artifact, though. It is generated output and can change after restore.
Lock Package Sources With Package Source Mapping
One of the strongest defenses against dependency confusion in NuGet-based builds is Package Source Mapping.
The basic idea is simple:
Internal package IDs
|
v
Private feed only
Public package IDs
|
v
Public feed only
For example:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="CompanyFeed"
value="https://packages.company.local/nuget/v3/index.json" />
<add key="nuget.org"
value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="CompanyFeed">
<package pattern="Contoso.*" />
<package pattern="Company.*" />
</packageSource>
<packageSource key="nuget.org">
<package pattern="*" />
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
The exact mapping should reflect the organization's package naming strategy.
The important principle is:
A package should not be allowed to come from an unexpected source merely because that source happens to contain a package with the same ID.
Be Careful With the Wildcard Pattern
A common mistake is to create mappings that effectively defeat the purpose of source mapping.
For example:
<packageSource key="CompanyFeed">
<package pattern="*" />
</packageSource>
This says that the private feed can provide every package.
That may be acceptable in some repository architectures, but it does not clearly express ownership boundaries.
A more controlled configuration explicitly maps internal namespaces:
<package pattern="Contoso.*" />
and then maps public packages to the public source.
The mapping strategy should be reviewed whenever a new internal package namespace is introduced.
Central Package Management Helps With Governance
Enterprise repositories often centralize package versions.
For example:
<ItemGroup>
<PackageVersion Include="Company.Logging"
Version="5.2.0" />
<PackageVersion Include="Company.Data"
Version="3.8.1" />
</ItemGroup>
Central package management does not by itself prevent dependency confusion.
However, it improves visibility and makes dependency governance easier.
You can combine:
Central package versions
+
Source mapping
+
Locked restore
+
CI validation
to create a stronger dependency boundary.
Detect Unexpected Package Sources in CI
Do not rely only on developer machines.
The CI environment should validate the package source configuration before restoring dependencies.
A simple pipeline step can inspect the repository configuration:
dotnet restore --locked-mode
when lock-file-based restore is part of the repository's dependency strategy.
The pipeline should also fail if:
An unexpected package source is configured.
A private package is mapped to a public source.
A package appears from an unapproved source.
Dependency metadata changes unexpectedly.
Restore behavior differs from the approved configuration.
The objective is to make dependency resolution deterministic and auditable.
Add an Allowlist for Internal Packages
A practical enterprise control is to maintain an internal package allowlist.
For example:
Company.Core
Company.Logging
Company.Security
Company.Data
Company.Messaging
The CI process can compare resolved dependencies against this inventory.
Conceptually:
Resolved package
|
v
Is package internal?
|
Yes
|
v
Is source approved?
/ \
Yes No
| |
Pass Fail
This approach is especially useful for large organizations with many repositories.
Detect Suspicious Version Changes
Version anomalies can provide an additional signal.
Suppose the repository previously resolved:
Company.Logging 3.4.1
and a new restore suddenly resolves:
Company.Logging 98.0.0
That should trigger investigation.
However, a higher version number is not automatically malicious.
An attacker may choose a high version, but legitimate packages can also undergo major version changes.
Use version anomalies as a detection signal rather than as proof of compromise.
Review Package Metadata
When investigating a suspicious package, examine:
Package ID
Version
Source
Authors
Repository metadata
Release date
Dependencies
Package ownership
Digital signatures where applicable
Changes from the previous trusted version
A suspicious package should be investigated before it is executed in a privileged build environment.
This is particularly important because package installation and build operations can execute code through package-related build mechanisms.
Protect CI Credentials
Dependency confusion is not limited to stealing source code.
A compromised package executing inside CI may potentially access credentials available to the build.
That makes credential design an important part of supply-chain security.
Avoid exposing unnecessary secrets to ordinary builds.
For example:
Pull Request Build
|
+-- Read-only package access
+-- No production credentials
+-- Minimal cloud permissions
versus:
Production Deployment
|
+-- Restricted credentials
+-- Protected environment
+-- Explicit approval
A dependency compromise should not automatically become a production compromise.
Add a Dependency Security Gate
A CI security gate can combine several checks.
Conceptually:
dotnet restore
|
v
Dependency inventory
|
+-----------+-----------+
| |
v v
Source validation Version validation
| |
+-----------+-----------+
|
v
Vulnerability scan
|
v
Policy check
|
+----+----+
| |
Pass Fail
| |
Build Stop
A practical policy might reject:
Unknown package source
Unexpected internal package
Unauthorized source mapping
Dependency lock mismatch
Known critical vulnerability
Unexpected dependency change
This turns dependency security into an automated build control rather than a manual review process.
Common Mistakes
Using Multiple Feeds Without Source Mapping
Simply adding a private feed alongside a public feed does not establish package ownership.
Assuming Private Package Names Are Secret
Package IDs should not be treated as credentials.
If an internal package name becomes known, an attacker may attempt to register the same name publicly.
Trusting Package Versions
A familiar package ID with an unexpected version still deserves investigation.
Checking Only Direct Dependencies
A compromised package can enter through the transitive dependency graph.
Allowing Developer-Specific NuGet Configuration
A developer's machine may contain additional package sources that are invisible in the repository.
Giving CI Excessive Permissions
A compromised dependency executing during CI should encounter as little privileged information as possible.
Treating Vulnerability Scanning as Dependency Confusion Protection
A malicious package can be completely unknown to vulnerability databases.
Dependency confusion requires source and package-identity controls, not just vulnerability scanning.
Troubleshooting Restore Failures After Source Mapping
Source mapping can expose dependency problems that were previously hidden.
For example:
NU1101
Unable to find package Company.Internal.Client.
If the package exists on the private feed, verify:
The package source is configured.
The package ID matches the mapping pattern.
Authentication is working.
The package version exists.
CI uses the same
NuGet.config.No unexpected configuration overrides the repository settings.
If a public package unexpectedly fails after mapping is introduced, inspect whether the package has been assigned to the correct public source.
The goal is not to make restore fail less often.
The goal is to make restore fail when dependency provenance is ambiguous.
Recommended Enterprise Detection Strategy
A mature .NET dependency-confusion defense should operate at multiple layers.
Repository Layer
Maintain:
NuGet.config
Package source mapping
Central package versions
Dependency lock files where appropriate
CI Layer
Validate:
Approved package sources
Dependency graph
Package versions
Lock-file consistency
Security findings
Infrastructure Layer
Control:
Private feed authentication
Package publishing permissions
CI network access
Build credentials
Artifact retention
Developer Layer
Provide:
Standard NuGet configuration
Documented internal package namespaces
Approved restore commands
Security guidance
No single control should be expected to catch every dependency supply-chain attack.
Frequently Asked Questions
Is dependency confusion the same as a vulnerable NuGet package?
No.
A vulnerable package contains a known security weakness.
Dependency confusion involves resolving the wrong package, often because an internal package name can be obtained from an unintended public source.
Can a package vulnerability scanner detect dependency confusion?
Not reliably.
A malicious package may be newly published and have no known vulnerability record.
Source mapping and package provenance controls are therefore important.
Is Package Source Mapping enough?
It is a strong control, but it should be combined with locked dependencies, controlled package sources, CI validation, and least-privilege build credentials.
Should internal packages be published publicly?
That depends on the organization's requirements.
If an internal package is not intended for public consumption, its package namespace and source should be controlled accordingly.
Should every NuGet package be manually reviewed?
That does not scale for most organizations.
Automated source controls, dependency policies, vulnerability scanning, and targeted human review provide a more practical approach.
Conclusion
Dependency confusion is fundamentally a package provenance problem. A .NET project can contain a perfectly legitimate PackageReference while the restore environment introduces an unintended package from an untrusted source.
The most effective defense is to make package ownership explicit. Internal package IDs should resolve only from approved internal feeds, public packages should resolve from approved public sources, and CI should continuously validate that dependency resolution follows those rules.
For enterprise .NET builds, the strongest approach combines NuGet source mapping, controlled package configuration, dependency inspection, locked restores, security gates, and least-privilege CI credentials. When these controls are automated, dependency provenance becomes part of the build itself rather than something a security team has to investigate after an unexpected package appears.

Join the conversation! Your thoughts help the community grow.