Safeguard
Software Supply Chain•

How do NuGet lock files and version pinning work?

Turn on RestorePackagesWithLockFile, restore in locked mode in CI, and pin the SDK so a NuGet build resolves the same package graph every time.

Safeguard Security Team
4 min read

NuGet pins your full package graph when you set RestorePackagesWithLockFile to true, which writes packages.lock.json. In CI, restore with --locked-mode so the build fails if the resolved graph differs from the lock file. Pair it with a pinned SDK, because implicit packages can change with the SDK version.

Why are NuGet versions not pinned already?

A PackageReference with Version="3.6.0" means "3.6.0 or higher", the lowest applicable version. The Microsoft documentation explains how a restore can change without any edit to your project. If you ask for 4.0.0 and the feed only has 4.1.0, NuGet resolves to 4.1.0. The next day, if 4.0.0 is published, NuGet finds the exact match and resolves to it. A version can also be removed from a repository that allows deletion, and NuGet then settles on the best remaining match.

Floating versions make it more visible: Version="3.6.*" is supported with PackageReference and deliberately takes the newest matching release.

Transitive dependencies are resolved at restore time, and the documentation notes that in PackageReference projects all sources must be available for every restore.

How do you turn on the lock file?

Set the MSBuild property RestorePackagesWithLockFile to true in a property group of the project file.

The documentation says restore then generates packages.lock.json at the project root, listing all package dependencies, the full closure. Commit it.

How do you make CI strict?

Locked mode. The docs give three forms: dotnet restore --locked-mode for the dotnet CLI, msbuild -t:restore -p:RestoreLockedMode=true, or the RestoreLockedMode property in the project file. When locked mode is on, restore either uses the lock file or fails if the lock file and the project disagree, so a changed package set cannot pass quietly.

A constructed example: a developer bumps a direct package in the project file and forgets to restore locally. CI runs in locked mode, finds the project and lock file disagree, and fails the build. The developer restores, reviews the lock file diff in the pull request and commits it.

What can still break locked restores?

The documentation notes that for some project types the .NET SDK adds implicit PackageReference items, and a different SDK version changes those package versions, causing locked mode failures. The mitigation it gives is a global.json with the rollForward policy set to disable. Everyone on the repo and CI then uses the same SDK version, and you decide when the implicit packages change.

How do auditing and source mapping fit?

NuGet audit reports known vulnerabilities in your packages. The documentation's feature table says NuGetAudit for PackageReference arrived with NuGet 6.8 (.NET 8 SDK), with later versions adding suppression and audit sources. Check which versions your build uses.

Package source mapping is the companion control. Added in NuGet 6.0, it lets you filter, per package, which sources NuGet will search, so an internal package name cannot be satisfied from a public feed. Microsoft notes a limit: it applies when NuGet downloads packages during restore, install and update, but commands such as dotnet list package and the Visual Studio package browser still send package IDs to all configured sources, including public ones. Know that gap when you design your feed layout.

How to check or apply this

  1. Set RestorePackagesWithLockFile for each project and commit packages.lock.json.
  2. Run dotnet restore --locked-mode in CI.
  3. Add global.json with a pinned SDK and rollForward disabled.
  4. Review lock file diffs in pull requests like source changes.
  5. Enable NuGet audit and decide the severity that fails the build.
  6. Configure package source mapping so internal names resolve only from your feed.
  7. Keep a software bill of materials for each release built from the lock file.

Quick check

Does Version="1.2.3" pin exactly 1.2.3?

No. It means 1.2.3 or higher. Use the lock file for the exact resolved graph.

What does locked mode do?

It makes restore use the lock file and fail when the lock file does not match the project, so the dependency graph cannot change silently.

Sources

Last reviewed: 18 October 2026.

Never miss an update

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

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.