Back to glossary

NuGet

NuGet is the package manager for .NET that distributes C# libraries as .nupkg archives through the nuget.org registry and restores them in a build.

Also known as NuGet Package Manager, NuPack, .NET package manager, nuget.exe

NuGet is the package manager for .NET: it distributes C# and other .NET libraries as .nupkg archives through the public nuget.org registry and restores their dependencies into a build. Because NuGet resolves packages inside an MSBuild build rather than through standalone install scripts, its supply chain risk lives at build time, assembly load, and JIT compilation.

What is NuGet?

#

NuGet is the official package manager for the .NET ecosystem, the equivalent of npm for Node.js or pip for Python. It shipped on October 5, 2010, originally under the name NuPack from the Outercurve Foundation, and is now maintained by Microsoft and the .NET Foundation under the Apache 2.0 license (Wikipedia). The public registry at nuget.org hosts hundreds of thousands of packages: nearly 450,000 as of Socket's 2025 .NET launch (Socket), and it served roughly 7.5 billion package downloads per week in October 2026 (nuget.org/stats).

A developer declares a dependency, runs a restore, and NuGet fetches the package and everything it transitively needs. What makes NuGet distinct from npm or pip is that it is woven into MSBuild, the .NET build engine. Running dotnet build restores and compiles in one step, and a package can contribute not just compiled code but build logic that executes as your project compiles. Microsoft's own overview calls a package “a single ZIP file with the .nupkg extension that contains compiled code (DLLs), other files related to that code, and a descriptive manifest”.

What is inside a .nupkg file?

#

A .nupkg file is an ordinary ZIP archive. Unzip one and you find a .nuspec manifest (ID, version, authors, dependencies), a lib/ folder of assemblies organized by target framework, and optional build/, content/, and analyzers/ folders. The build/ folder is the one that matters for security: files placed there are imported directly into the consuming project's MSBuild build.

We packed a minimal library with the .NET 10 SDK to show the real layout. The package ships one assembly and one build/*.targets file:

Plain text
HelloPkg.1.0.0.nupkg
├── HelloPkg.nuspec
├── lib/net10.0/HelloPkg.dll
├── build/HelloPkg.targets        # imported into the CONSUMER build
└── [Content_Types].xml

Nothing here is unusual, and nothing here runs on download. The .dll and the .targets file only matter once a consumer builds and runs against the package, which is exactly where the trust assumption breaks.

How does NuGet install and resolve dependencies?

#

NuGet resolves dependencies at restore time and caches packages in a per-user global packages folder so multiple projects share one copy. Modern projects use PackageReference, which lists only top-level dependencies in the .csproj and computes the full graph during restore; the older packages.config format wrote a flat list instead.

Two resolution rules decide which version wins, and both lean toward the lowest version that satisfies a constraint. For transitive dependencies, NuGet applies the “lowest applicable version” rule: it “restores the lowest possible version of a package as defined by its dependencies”. When versions conflict within a subgraph, a direct reference overrides a transitive one (“direct dependency wins”). These defaults matter for dependency confusion, where an attacker publishes a public package that shadows a private one, and for understanding why a lockfile (packages.lock.json) is worth enabling: it pins the resolved graph so an unexpected version substitution is visible in a diff.

Where does package code run if NuGet has no install scripts?

#

NuGet has no npm-style postinstall scripts. The legacy PowerShell hooks install.ps1 and init.ps1 run only in old packages.config projects; under PackageReference they are ignored. Microsoft's own warning NU5120 states the file “will not be executed if the package is installed to a package reference type of project.” So on a modern restore, no package-supplied code runs at all.

That is the good news, and it is a real design improvement over registries where arbitrary code runs on install. The catch is that execution did not disappear; it moved to two later points. First, a package's build/*.props and build/*.targets files are imported into the consumer's MSBuild build and run as the project compiles. Second, once an assembly loads, the CLR runs its module initializers and type static constructors, and JIT-compiles its methods, all before application code executes.

We reproduced both with the .NET 10 SDK. A [ModuleInitializer] in the package and a BeforeTargets="Build" target in build/HelloPkg.targets were enough to run package-controlled code during the consumer's build and before its Main:

C#
// In the package: runs on assembly load, before the host's Main
[ModuleInitializer]
public static void Run() =>
    Console.WriteLine("[HelloPkg] module initializer ran on assembly load");
Plain text
$ dotnet build Consumer.csproj
  [HelloPkg] build/.targets executed inside the consumer build (MSBuild)
Build succeeded.

$ dotnet run --project Consumer.csproj
[HelloPkg] module initializer ran on assembly load (before Main)
[Consumer] Main started; Lib.Answer=42

The figure shows the four points where package code can run: restore (none), build (MSBuild props and targets), assembly load (module initializers, static constructors, and JIT), and runtime. Steps 2 and 3 are the execution surface. In our test on the .NET 10 SDK, a shipped .targets file printed during the consumer's build, and the package's module initializer printed before the consumer's Main ran.

This is not a theoretical surface. The four malicious NuGet packages targeting ASP.NET developers that Socket's Threat Research Team analyzed used a static constructor to hook the JIT compiler on load, so, as the writeup puts it, “the malware executes before testing begins.” The same pattern drove the five NuGet packages impersonating Chinese UI libraries, where a module initializer patched clrjit.dll!getJit to decrypt an infostealer. Socket's analysis stressed that “installing a package is not required for execution: restoring the package to the local cache and loading the DLL in any .NET host process is sufficient.” A clean lockfile pins what you restored, but it does not prove that what loaded was safe.

How attackers get a malicious NuGet package installed

#

Executing code is only half an attack; the other half is getting a developer to reference the package. On NuGet the dominant technique is typosquatting and brand impersonation, often sharpened with Unicode homoglyphs. The Nethereum.All homoglyph typosquat swapped a Cyrillic “е” (U+0435) into the name of Nethereum, the standard .NET Ethereum library, to pass casual inspection before exfiltrating wallet keys. It stayed live for roughly four days between publication and takedown.

Download counts, the number developers glance at to judge trust, are cheap to forge. The Braintree.Net credit-card skimmer reported about 14 million downloads, but Socket found roughly 11 million of those were padding sprayed across 120 empty 0.0.x placeholder versions published in a single day; the genuinely malicious releases had only about 334 real installs. That is a ~32,900x gap between the headline number and the real blast radius. The same five impersonated UI libraries ran a related trick: 219 of their 224 published versions carried listed: false, hiding them from search while keeping them installable by pinned version and invalidating hash-based blocklists on every rotation.

Dwell time can be long. The Tracer.Fody.NLog wallet stealer impersonated a trusted IL-weaving library and sat on the registry for more than five years with roughly 2,000 downloads before discovery. And the package you install is not always the code you read: the Sicoob SDK impersonation published a clean-looking GitHub source while the distributed .dll carried credential-exfiltration logic the public repository never contained. The lesson is a house position we hold across ecosystems: analyze what is actually in the package, not what its metadata or its linked repository claims.

What NuGet's trust controls do and do not cover

#

NuGet has real integrity and identity controls, but each has a boundary worth stating plainly. Author and repository code signing proves who published a package and that it was not altered in transit; it says nothing about whether the code is safe, and a compromised maintainer signs malware with a valid certificate. ID prefix reservation stops a stranger from publishing under a reserved prefix, but the NuGet team was explicit that the goal is identity, not safety. As NuGet's Daniel Jacobson put it, “what we are striving for is clear messaging on identity of the package and the source of the package” (NuGet blog).

ControlWhat it stopsWhat it does not cover
Package signingTampering in transit; forged publisher identityMalicious code from a legitimate or compromised publisher
ID prefix reservationStrangers publishing under a reserved prefixTyposquats on unreserved names; a trusted author turning malicious
packages.lock.jsonSilent version substitution between restoresCode that executes on build or load from a pinned, malicious version
CVE / advisory scanningKnown, disclosed vulnerabilitiesMalware, which is never assigned a CVE
Behavioral package analysisInstall-time, build-time, and load-time behavior before it runsNovel logic that evades static and dynamic detection

The last two rows carry the sharpest trade-off. Vulnerability scanning against the CVE and advisory feeds is necessary but structurally blind to malicious packages: malware does not get a CVE, so a registry that is “clean” by CVE count can still ship a credential stealer. That is why the defensible position is to inspect behavior before code reaches a build. Socket analyzes the package contents themselves, flagging JIT-hooking module initializers, install scripts, and unexpected network or filesystem access, and Socket Firewall mediates dependency requests so a known-malicious package is blocked before the package manager fetches it, rather than quarantined after it has already run on a build agent.

NuGet removed the install-script footgun that still haunts other registries, and that is a genuine win. But the supply chain attacks that hit NuGet in 2025 and 2026 show where the risk went instead: a build/*.targets file that runs when you compile, and a module initializer that runs the instant the CLR loads an assembly. Neither appears as an “install” step, and neither shows up in a lockfile diff or a CVE scan. On .NET, the question is not whether a dependency was installed safely. It is what already ran the moment you built against it.