Open Source Security

NuGet Central Package Management Security

Central Package Management pulled NuGet's multi-project version chaos into a single source of truth. The security implications run deeper than the ergonomics suggest.

Shadab Khan
Security Engineer
6 min read

Before Central Package Management, a large .NET solution could carry the same NuGet package at three different versions across a dozen projects without anyone noticing. CPM, which became generally available with .NET SDK 6.0.300 and stabilised through 2022 and 2023, collapses this. A single Directory.Packages.props at the solution root declares the version. Individual projects reference the package without a version. One file becomes the source of truth for every package in every project. As an ergonomics improvement, CPM is uncontroversial. The supply chain implications go further than that, and this post is the practitioner's read on them.

What problem was CPM actually solving?

Version drift in large .NET codebases. A typical enterprise solution with 50+ csproj files under it accumulated the same package at subtly different versions in different projects — Newtonsoft.Json at 12.0.3 in the API project, 13.0.1 in the shared library, 13.0.3 in the tests. Each version carried its own CVE exposure, its own behaviour quirks, its own license implications. Patching required editing every csproj and hoping you caught them all. CPM makes this a one-file change.

The security payoff is real but understated by Microsoft's original announcement. CPM is not just more convenient; it fundamentally changes the shape of the auditable surface.

How does CPM change supply chain auditability?

Three specific ways:

Single source of truth for versions. An SBOM generated from a CPM-managed solution has one version per package, no ambiguity. Pre-CPM SBOMs often had to reconcile multi-version occurrences and the resulting dependency graph was harder to reason about.

Policy enforcement is file-scoped. You can write a PR policy that forbids specific versions of specific packages in Directory.Packages.props, and that policy covers every project in the solution. Pre-CPM, you had to enforce the same policy against every csproj.

Drift detection becomes trivial. A PR that updates a version is visible as a single diff. Pre-CPM, a PR that intended to bump Newtonsoft.Json solution-wide might actually only bump it in some projects, leaving drift behind.

These changes make the auditable surface materially smaller and more trustworthy.

What threats does CPM not address?

The version drift problem is not the same as the compromised version problem. CPM ensures everyone uses the same version; it does not ensure that version is safe. A Directory.Packages.props that references Newtonsoft.Json at a specific version still needs to be reviewed for whether that version has known CVEs, whether the author's key is trustworthy, and whether the package has been tampered with.

Similarly, CPM does not prevent dependency confusion. If your Directory.Packages.props references an internal package name that is also typo-squatted on the public NuGet feed, the wrong package can still be pulled depending on feed ordering.

The takeaway: CPM is a precondition for defensible NuGet supply chain, not a complete solution.

How do you layer supply chain policies on top?

Three patterns that work in production .NET 8 environments:

Feed priority ordering. In NuGet.Config, internal feeds should always resolve before public ones. CPM does not do this for you; it is feed-layer configuration.

Package source mapping (PSM). PSM, a feature that came into NuGet alongside CPM, lets you declare "package Contoso.Internal.* always resolves from the internal feed, never from public NuGet." This closes the dependency confusion gap. CPM + PSM is the full defensive posture.

CentralPackageTransitivePinningEnabled. Turning this on in Directory.Packages.props means transitive dependencies of your direct references also honour your central version overrides. Without it, transitive versions drift outside your policy scope.

A Directory.Packages.props that uses all three of these is meaningfully harder to attack through the NuGet supply chain than the pre-CPM norm.

What about version conflicts?

CPM's central file can surface conflicts that were previously hidden. Package A requires System.Text.Json at ≥7.0.0; package B has been tested only up to 6.0.11. Pre-CPM, different projects could pick different versions and the issue was invisible until runtime. With CPM, the choice is explicit and forces a resolution during development.

From a supply chain perspective, this is a feature, not a drawback. Version conflicts represent real compatibility risk; making them visible at build time is the correct behaviour.

Migration is a one-day project for most solutions

The migration from per-project versions to CPM is mechanical. The CentralPackageManagement MSBuild tooling includes a migration command, and for most solutions the manual cleanup that remains is small — removing <Version> attributes from <PackageReference> elements across projects and writing the Directory.Packages.props file with the resolved versions.

For solutions of meaningful size (>20 projects), budget a day. For solutions with unusual conventions (mixed SDK-style and classic csproj, private-feed-specific quirks), budget a week. The return is immediate: every subsequent supply chain audit is easier.

What does the SBOM pipeline look like post-CPM?

Simpler. A CPM-aware SBOM generator reads Directory.Packages.props once and traces per-project references back to that central file. Dependencies are enumerated once, with single versions. CycloneDX generation via dotnet list package --format json plus a small transformation, or via CycloneDX.BomModule / CycloneDX/cyclonedx-dotnet, produces clean output.

Store the SBOM in the same release artefact bundle as the build output. For EU CRA or federal SSDF reporting, a CPM-managed solution has a materially cleaner story to tell than one that is not.

A practical CPM adoption checklist

  • Migrate from per-project versions to Directory.Packages.props.
  • Enable CentralPackageTransitivePinningEnabled.
  • Configure Package Source Mapping for any internal packages.
  • Set feed priority: internal feeds resolve before public.
  • Generate SBOMs from the CPM-managed solution and verify they look right.
  • Write PR policies that review changes to Directory.Packages.props with elevated scrutiny (this is now the single most important file in the solution from a supply chain perspective).

Each step takes hours. The combined effect is a .NET supply chain posture that is credible under any of the 2025 regulatory frameworks.

How Safeguard Helps

Safeguard's .NET support is CPM-aware — SBOM generation reads Directory.Packages.props as the authoritative version source and correlates it with Package Source Mapping to detect dependency-confusion risk. Policy gates can restrict changes to Directory.Packages.props to specific reviewers and enforce that no version with an open KEV-listed CVE can land there. Griffin AI summarises CPM migration status across a portfolio of .NET solutions and flags solutions that have drifted back to per-project versions (a common regression). For .NET shops running many solutions across multiple repos, Safeguard makes CPM adoption a tracked supply chain property rather than a per-team discipline.

Never miss an update

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