Your .NET application's attack surface is not just the code you write — it's every NuGet package you pull, every package those packages pull, and the feeds and build machines in between. The SolarWinds SUNBURST attack of 2020 was the wake-up call for the .NET ecosystem: attackers didn't exploit a CVE, they compromised the build pipeline of a trusted vendor and shipped signed malware to thousands of organizations. NuGet supply-chain security is about controlling what enters your build and proving it's what you expected. This post covers the specific attack classes and the specific configuration that defends against each.
How do NuGet supply chain attacks actually happen?
They happen at four points: the package, the name, the feed, and the build. Understanding the mechanism tells you which control to reach for.
- Malicious or backdoored packages — a package that does something harmful, either from the start (typosquatting) or after a maintainer takeover or a compromised publish.
- Dependency confusion — your build resolves an internal package name from the public nuget.org feed because an attacker registered that name there with a higher version number. Alex Birsan demonstrated this class of attack in 2021 across multiple ecosystems, and package managers that check multiple feeds without name pinning are exposed.
- Typosquatting — a package named
Newtonsoft.JsoonorMicrosoft.AspNetcorethat a developer installs by mistake. - Build-time execution — MSBuild targets and package install scripts can run code during restore/build, so a malicious dependency doesn't need to wait until runtime.
Defense 1: Pin what you resolve with package source mapping
Dependency confusion is defeated by telling NuGet exactly which feed each package pattern comes from. Package source mapping, available since NuGet 6.0 (shipped with .NET 6), does this in nuget.config:
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="internal-feed">
<package pattern="Contoso.*" />
</packageSource>
</packageSourceMapping>
Now Contoso.Billing can only ever come from your internal feed, no matter what a same-named package on nuget.org claims. This one file is the single highest-value control against dependency confusion.
Defense 2: Make restores reproducible with lock files
A lock file records the exact resolved version and content hash of every package, direct and transitive, so a compromised mirror or a floating version can't silently swap something in. Enable it per project:
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
Commit the generated packages.lock.json, then enforce it in CI so any drift fails the build:
dotnet restore --locked-mode
With --locked-mode, restore refuses to run if the lock file and the project's declared dependencies disagree — turning a silent substitution into a loud failure.
Defense 3: Audit and verify what you already depend on
Since .NET 8, dotnet restore runs NuGet Audit against the GitHub Advisory Database and warns on known-vulnerable packages; .NET 9 extended this to transitive dependencies by default. Elevate the warnings and run an explicit listing in CI:
dotnet list package --vulnerable --include-transitive
dotnet list package --deprecated
Require package signature verification so an unsigned or tampered artifact is rejected, and prefer feeds that enforce author or repository signing. Continuous software composition analysis for your NuGet tree fills the gap between advisory databases — which lag — and the packages you actually ship, and gives you reachability context so you know which advisories touch your code.
Defense 4: Know exactly what you shipped with an SBOM
You cannot respond to "are we affected by the new X vulnerability?" without an accurate inventory. Generate a CycloneDX or SPDX software bill of materials on every build and store it as a build artifact. When the next widely-exploited package issue lands, an SBOM turns an all-hands repo grep into a database query. SBOM generation and management tooling automates producing, storing, and diffing these across releases so you can also see when a transitive dependency changed and why.
A NuGet hardening checklist
| Control | Mechanism | Defends against |
|---|---|---|
| Source mapping | packageSourceMapping in nuget.config | Dependency confusion |
| Lock files | RestorePackagesWithLockFile + --locked-mode | Silent substitution, floating versions |
| Signature verification | Signed-package enforcement on the feed | Tampered/unsigned artifacts |
| NuGet audit | NuGetAudit + warnings-as-errors | Known-vulnerable packages |
| Private feed hygiene | 2FA on publishing accounts, scoped API keys | Account/publish compromise |
| SBOM per build | CycloneDX/SPDX generation | Slow incident response |
-
packageSourceMappingpins internal patterns to the private feed - Lock files committed; CI runs
--locked-mode -
dotnet list package --vulnerablegate in the pipeline - Signature verification and scoped, rotated feed keys
- SBOM produced and retained on every build
How Safeguard Helps
Safeguard watches the NuGet layer continuously so the controls above stay enforced instead of documented. It ingests your dependency graph, monitors it against fresh advisory feeds, and uses reachability analysis to separate the flagged CVEs that touch executed code from the ones buried in a package method you never call. When a dependency-confusion risk or a newly disclosed malicious package appears, you're alerted with the affected projects already identified. You can run it from the Safeguard CLI directly in CI to fail builds on policy violations, and generate or ingest SBOMs automatically so incident response is a lookup, not a scramble. Teams consolidating supply-chain tooling frequently compare it on the Safeguard vs Snyk and Safeguard vs Black Duck pages.
Create a free project at app.safeguard.sh/register and read the NuGet integration guide at docs.safeguard.sh.