Safeguard
Engineering

Securing the .NET NuGet Supply Chain

Package source mapping, packages.lock.json, NuGetAudit and signature verification — .NET ships more built-in supply chain controls than any other ecosystem. Most teams enable none of them.

Elena Kovacs
Compliance Architect
6 min read

Securing the NuGet supply chain means enabling four controls that already ship in the .NET SDK: package source mapping to kill dependency confusion, packages.lock.json with locked-mode restore for integrity, NuGetAudit to fail builds on known CVEs, and signature verification to authenticate publishers. None of them require third-party tooling and none are on by default. That combination — excellent controls, zero adoption pressure — is why .NET shops routinely pass compliance audits while running restores that would accept a spoofed internal package from nuget.org without blinking.

Package source mapping: the dependency confusion fix

Most .NET organizations run at least two feeds: nuget.org and an internal one (Azure Artifacts, GitHub Packages, a self-hosted BaGet). Without source mapping, NuGet queries all of them and a public package named like your internal one — Contoso.Payments.Core uploaded to nuget.org by an attacker — is in play. This is the classic dependency confusion setup, and .NET's internal-naming conventions make internal package names highly guessable.

Since NuGet 6.0, nuget.config can pin name patterns to sources:

<packageSourceMapping>
  <packageSource key="nuget.org">
    <package pattern="*" />
  </packageSource>
  <packageSource key="internal-feed">
    <package pattern="Contoso.*" />
  </packageSource>
</packageSourceMapping>

With that in place, Contoso.* can only resolve from the internal feed, no matter what exists publicly. Commit the nuget.config at repo root so the mapping travels with the code, and — belt and suspenders — reserve your Contoso. ID prefix on nuget.org, which blocks anyone else from publishing under it and gets you the verified checkmark.

Lock it: packages.lock.json and locked-mode restore

NuGet resolves floating and range versions at restore time unless you tell it not to. Opt into lockfiles per project:

<PropertyGroup>
  <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>

That writes packages.lock.json with exact versions and SHA-512 content hashes for the full transitive closure. Then make CI enforce it:

dotnet restore --locked-mode

Locked mode fails the build if resolution would differ from the lockfile or if a downloaded package's hash doesn't match — which is your tamper detection for compromised feeds and man-in-the-middle scenarios. Pair it with Central Package Management (Directory.Packages.props) so versions are declared once per repo instead of drifting across forty .csproj files; version sprawl is how "we patched that CVE" turns out to be true for six projects out of nine.

NuGetAudit: CVE gating built into restore

Since NuGet 6.8 (bundled with the .NET 8 SDK), every restore can audit resolved packages against the GitHub Advisory Database:

<PropertyGroup>
  <NuGetAudit>true</NuGetAudit>
  <NuGetAuditMode>all</NuGetAuditMode>
  <NuGetAuditLevel>high</NuGetAuditLevel>
  <WarningsAsErrors>$(WarningsAsErrors);NU1903;NU1904</WarningsAsErrors>
</PropertyGroup>

NuGetAuditMode set to all includes transitive packages (direct-only is the older default and misses most real findings). The NU1901NU1904 warnings map to low-through-critical severities; promoting the high and critical ones to errors turns restore into a security gate with no extra CI step. For ad-hoc checks, dotnet list package --vulnerable --include-transitive gives you the same data in human-readable form.

Honest limitation: this is advisory-lag detection. A malicious package with no published advisory passes NuGetAudit cleanly. Compromised author accounts have shipped malware under legitimate, previously-clean package IDs — the window between publish and advisory is where audit-based tooling goes blind, and where registry-monitoring SCA earns its cost by flagging anomalous publishes (owner changes, first-time native binaries, obfuscated init code) rather than waiting for a CVE number.

Signatures: useful, with caveats

nuget.org repository-signs every package it serves, and authors can additionally author-sign. Verification is off by default; turn it on where it matters:

dotnet nuget verify Contoso.Payments.Core.1.4.2.nupkg

and in nuget.config, signatureValidationMode set to require plus a trustedSigners section listing the repository certificate and any author certs you rely on. What this buys you: proof the package came through nuget.org unmodified, and (for author signatures) that the publisher held a specific certificate. What it doesn't: repository signing says nothing about whether the account that published was compromised — the 2023-2024 waves of malicious packages on nuget.org were all repository-signed, because everything on nuget.org is.

One operational warning: strict signature validation plus an internal feed that re-hosts packages (rather than upstream-proxying them) will fail validation on packages that lost their repository signature in transit. Test the policy against your actual feed topology before enforcing it, preferably not on release day.

SBOMs and the compliance layer

If you sell software, your customers increasingly ask what's inside it. dotnet builds integrate cleanly with CycloneDX via the CycloneDX.MSBuild package or the standalone dotnet-CycloneDX tool, and lockfiles make the output deterministic. Generate the SBOM in the release pipeline — not on request, months later, from an archaeology exercise — and manage the artifacts somewhere queryable like SBOM Studio so "are we exposed to X" is a lookup rather than a project. Safeguard customers typically wire the SBOM generation and the NuGetAudit gate into the same pipeline stage, which keeps the inventory and the enforcement honest with each other.

Rollout order for an existing codebase

  1. Commit nuget.config with explicit sources and package source mapping. (Hours; kills dependency confusion.)
  2. Reserve your ID prefix on nuget.org. (One-time; free.)
  3. Enable lockfiles and --locked-mode in CI. (A day, including the lockfile-churn conversations.)
  4. Turn on NuGetAudit with transitive mode; promote NU1903/NU1904 to errors. (Hours, plus triage of the initial findings — budget real time here.)
  5. Add signature validation and SBOM generation to release builds. (A day.)

Steps 1 and 3 remove attack classes; steps 4 and 5 add detection and evidence. Doing them in that order means the noisy step (audit triage) lands on an already-stabilized graph.

Frequently asked questions

Does package source mapping work with transitive dependencies?

Yes — every package in the graph, direct or transitive, must match a pattern mapped to the source it resolves from, or restore fails. That's exactly the behavior you want, though it means the initial rollout will surface packages you didn't know you pulled publicly.

Is packages.lock.json worth the merge-conflict churn?

For applications and services, yes: locked-mode restore is the only built-in defense against tampered or re-resolved packages between environments. Central Package Management reduces the churn substantially by collapsing version declarations to one file.

Why did a malicious package pass NuGetAudit?

NuGetAudit checks the GitHub Advisory Database, so it only knows about vulnerabilities and malware that have been reported and published. Fresh compromises pass until an advisory lands — closing that window requires registry-level monitoring, not advisory lookups.

Do repository signatures mean nuget.org packages are trustworthy?

They prove integrity and origin — the bytes came through nuget.org unaltered. They prove nothing about the publisher's account security or intent; repository-signed malware is routine, so signatures complement rather than replace scanning.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.