All articles

Fixing NU1902 from a transitive NuGet dependency you don't control

When a new advisory lands against a package you only get transitively, NU1902 can turn a green build red overnight. If your projects set TreatWarningsAsErrors, the audit warning becomes a restore error. The smallest safe fix is to add a direct PackageReference to the patched version, manage its version centrally, and mark it PrivateAssets="all" so the pin stays out of the package you publish.

Why did restore start failing with no code change?

NuGet audit (on by default since .NET 8 SDK) checks every package in the resolved graph against the GitHub Advisory Database. A moderate-severity hit reports as NU1902, high as NU1903, critical as NU1904. These are warnings, so most builds carry on. But TreatWarningsAsErrors promotes them to errors at restore time, before a single line of your code is compiled.

That is what made this one confusing. Nothing in the repository had changed. An advisory had been published against an old MySQL driver, and two shared UI libraries in a package repository both set TreatWarningsAsErrors. Both CI jobs failed on restore.

Where does the vulnerable version come from?

Start by asking NuGet rather than guessing:

dotnet list package --vulnerable --include-transitive
dotnet nuget why MyLibrary.csproj MySqlConnector

The second command (available from .NET 8.0.4xx) prints each path from your project to the package. In this case the old driver version arrived through an internal utilities package, which depended on Pomelo.EntityFrameworkCore.MySql 8.0.3, which in turn asked for an older MySqlConnector. Nobody on the team owned any link in that chain except the top one, and waiting for the top to be re-released was not an option for a red CI.

What are the options?

  1. Suppress the warning with NoWarn or WarningsNotAsErrors. It is quick, but you are now ignoring a real advisory and the suppression tends to outlive the problem.
  2. Turn audit off with NuGetAudit set to false. This hides every future advisory too, so I would not.
  3. Upgrade the parent package. The right long-term answer, but only if a release exists that has moved to a patched version.
  4. Pin the transitive package yourself. Reference the patched version directly. NuGet's "nearest wins" rule means your direct reference beats the version the transitive chain asked for.

The team went with the fourth option. It fixes the cause, rather than silencing the report, and it can be deleted once the parent catches up.

How do you pin it without leaking into your package?

If the project is itself packed and published, a plain direct reference has a side effect: the pin becomes a dependency in the generated .nuspec, and every consumer inherits your choice. For a library that is rarely what you want. PrivateAssets="all" tells NuGet that no assets from the reference flow to consumers, which also drops it from the package's dependency list.

With Central Package Management, the version lives in one place:

<!-- Directory.Packages.props -->
<Project>
  <ItemGroup>
    <PackageVersion Include="MySqlConnector" Version="2.6.1" />
  </ItemGroup>
</Project>

and each affected project references it without a version:

<!-- MyLibrary.csproj -->
<ItemGroup>
  <PackageReference Include="MySqlConnector" PrivateAssets="all" />
</ItemGroup>

There is also CentralPackageTransitivePinningEnabled, which pins transitive versions from the central file without adding a reference to each project. I prefer the explicit reference for a short-lived fix, because it is visible in the project that needed it and obvious to delete. If you go the transitive-pinning route, check what it does to the packed dependencies before you ship.

What should you check afterwards?

A green restore proves less than it looks. I would run these three checks:

  • Re-run dotnet list package --vulnerable --include-transitive and confirm the advisory has gone.
  • Run dotnet pack and open the resulting .nuspec inside the .nupkg. The pinned package should not appear in the dependency groups.
  • Run the tests that exercise the pinned package. A jump across minor versions of a driver is usually compatible, but "usually" is a thing to test, not assume.

One more thing to remember is that this fixes your build, not your consumers'. An application that pulls your library and the same old driver through another route still sees the advisory in its own graph. It has to apply its own pin or upgrade.

Key takeaways

  • NU1902 fails restore only when warnings are treated as errors, so a new advisory can break a build with no change to the repository.
  • Use dotnet nuget why to find which package drags in the vulnerable version before choosing a fix.
  • A direct PackageReference to the patched version overrides the transitive one, because NuGet resolves the nearest version.
  • Add PrivateAssets="all" in a library so the pin does not become a dependency of your published package, and confirm it by inspecting the nuspec.
  • Prefer pinning over suppressing or disabling audit, and treat the pin as temporary: remove it once the parent package ships a patched dependency.