Safeguard
Open Source Security

Case study: a malicious NuGet package compromise and its ...

Inside the Moq/SponsorLink malicious NuGet package incident: what shipped, who was exposed, and how to harden your .NET build pipeline against the next one.

Aman Khan
AppSec Engineer
8 min read

In August 2023, the .NET ecosystem got a sharp reminder that trust in a package registry is only as strong as the weakest link in its maintainer chain. This malicious nuget package incident didn't involve a nation-state actor breaching NuGet.org's infrastructure — it involved Moq, one of the most widely used mocking libraries in .NET testing, quietly shipping a dependency that fingerprinted developers' identities and phoned home without meaningful consent. Moq sits in hundreds of thousands of CI pipelines and enterprise test suites, and its 4.20.0 release turned a routine dotnet restore into a silent data-collection event for anyone who pulled it in.

What makes this case worth revisiting is not that it was the most technically sophisticated nuget supply chain incident on record — it wasn't a remote code execution exploit — but that it exposed a structural blind spot: build-time code (Roslyn analyzers, MSBuild tasks) runs with the same trust and privilege as your application, yet almost no organization inventories or monitors it the way they monitor runtime dependencies.

What Happened: Inside the Malicious NuGet Package Incident

Moq is maintained primarily by Daniel Cazzulino (known as "kzu"), a long-time and well-regarded figure in the .NET open source community — which is itself part of why this incident was so unsettling. There was no compromised account, no typosquatted package name, no phishing email. The maintainer of a legitimate, trusted package shipped the problematic code himself, as part of a monetization experiment called SponsorLink.

Starting with version 4.20.0, Moq added a dependency on the SponsorLink package. SponsorLink was distributed as a compiled, obfuscated DLL packaged as a Roslyn analyzer — code that executes automatically during compilation inside Visual Studio, dotnet build, and CI pipelines, independent of whether any test using Moq's mocking features ever actually ran. Developers who decompiled the analyzer found that it read the user's local Git configuration, extracted their email address, hashed it with SHA-256, and sent that hash to a remote endpoint, ostensibly to check GitHub Sponsors status. There was no opt-in prompt, no clear disclosure in release notes proportionate to what the code did, and the binary was deliberately obfuscated, which made independent review difficult — a red flag in any dependency, let alone one this popular.

The community reaction was swift and loud. Developers on GitHub, Hacker News, and Reddit characterized the behavior as spyware-like, noted the obfuscation as inconsistent with open source norms, and pointed out that corporate users behind restrictive firewalls would see build failures or silent network calls they couldn't explain. Some security-conscious teams treated it, correctly, as a dotnet backdoored package scenario for triage purposes: an unexpected, undisclosed network call embedded in build tooling is exactly the kind of indicator that a supply chain compromise playbook is designed to catch, regardless of the ultimate intent behind it.

Affected Versions and Components

  • Moq 4.20.0 and 4.20.1, published to NuGet.org in August 2023, both pulled in the SponsorLink analyzer by default.
  • Any solution referencing Moq transitively was exposed too — plenty of test-helper and fixture NuGet packages depend on Moq, so teams that never directly referenced Moq in a .csproj could still have inherited the analyzer through a second- or third-order dependency.
  • The trigger point was compilation, not test execution. Because SponsorLink ran as a build-time analyzer, simply restoring and building a project was enough to invoke it — you did not need to instantiate a single mock object.
  • Remediation shipped in Moq 4.20.2, which walked back the forced SponsorLink integration and made participation strictly opt-in. Later releases removed the dependency path entirely as the maintainer responded to sustained pushback.

If your dependency tree still resolves to 4.20.0 or 4.20.1 anywhere — including in test-only projects that don't ship to production — that's a live finding worth closing today, not archaeology.

CVSS, EPSS, and KEV Context

Here's the uncomfortable part for anyone used to triaging by score: no CVE was ever filed for the Moq/SponsorLink episode, and consequently there is no CVSS base score, no EPSS exploitation-probability estimate, and no CISA Known Exploited Vulnerabilities listing to consult. That's not an oversight in this analysis — it's a real gap in how the vulnerability ecosystem classifies harm. CVE and CVSS were built to describe exploitable memory corruption, injection, and access-control flaws with a defined attack vector. They weren't designed to score "a trusted maintainer shipped undisclosed telemetry that collects developer PII without consent," even though the operational impact — unauthorized data exfiltration from your build environment — looks a lot like the outcome of an actual compromise.

This matters because teams that gate remediation purely on "is there a CVE with a CVSS above X" will walk right past incidents like this one. The broader 2023 threat landscape reinforced the point: researchers also documented phishing campaigns targeting NuGet package maintainer credentials that year, aimed at hijacking legitimate publisher accounts to push malicious updates to trusted packages — a pattern more consistent with what people picture when they hear "nuget.org security breach," even though in this specific case the registry's infrastructure and account systems were never actually compromised. The Moq case shows that you don't need a broken authentication system or a stolen token to have a supply chain incident; you just need one maintainer decision that outpaces user consent.

Timeline

  • Early-to-mid August 2023 — Moq 4.20.0 is published to NuGet.org, introducing the SponsorLink dependency by default.
  • Within days — Developers inspecting build output and decompiling the SponsorLink DLL identify the Git-email-hashing and phone-home behavior; discussion escalates rapidly on the Moq GitHub repository, Hacker News, and Reddit's .NET communities.
  • Maintainer response — kzu initially defends SponsorLink as a legitimate, privacy-preserving sponsorship-verification mechanism, but the obfuscation and lack of explicit opt-in draw continued criticism from downstream consumers, including enterprise users citing internal policy violations.
  • Rapid follow-up release — Moq 4.20.2 ships shortly after, converting SponsorLink from a forced default into an opt-in feature.
  • Subsequent releases — later Moq versions drop the SponsorLink dependency path entirely as the project moves on from the experiment, and the incident becomes a frequently cited case study in .NET and broader open source governance discussions about consent and build-time code execution.

Remediation Steps

  1. Pin and verify Moq's resolved version. Check packages.lock.json, .csproj PackageReference ranges, and any transitive references to confirm you're not resolving to 4.20.0 or 4.20.1 anywhere in the dependency graph, including test-only projects.
  2. Enable NuGet lockfile mode. Use <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> and dotnet restore --locked-mode in CI so a maintainer publishing a new version can't silently change what your build pulls in without a reviewed diff to packages.lock.json.
  3. Audit build-time egress. Review CI network logs for outbound calls from build agents to unfamiliar domains during restore/build steps — not just at runtime. This is the layer most SCA tooling ignores entirely.
  4. Generate and diff SBOMs on every build. A CycloneDX or SPDX SBOM for .NET solutions (via dotnet-CycloneDX or equivalent) should capture analyzer packages and build-time dependencies, not only runtime assemblies referenced by the final artifact.
  5. Treat analyzers and MSBuild tasks as first-class attack surface. Apply the same source, signature, and publisher-identity review to Roslyn analyzer packages that you'd apply to any dependency that executes with developer or CI privileges.
  6. Review maintainer and publish-account changes over time, not just the diff between two package versions — a stable, well-known package changing hands or changing behavior abruptly is itself a signal worth alerting on.

How Safeguard Helps

Incidents like the Moq/SponsorLink case are exactly why point-in-time CVE scanning isn't enough for software supply chain security. Safeguard continuously monitors your NuGet, npm, and PyPI dependency graphs — including transitive and build-time-only packages like Roslyn analyzers — and flags new dependencies, version bumps, and behavioral changes as they land, rather than waiting for a CVE to be filed weeks or months later.

Specifically, Safeguard's platform helps teams get ahead of the next malicious nuget package incident by:

  • Tracking full dependency provenance, including build-time analyzers and MSBuild tasks that traditional SCA scanners often miss because they only inspect what ships in the final binary.
  • Generating and diffing SBOMs automatically on every build, so a newly introduced transitive dependency — like SponsorLink was for Moq consumers — surfaces as a reviewable change instead of a silent surprise.
  • Correlating unexpected build-agent network egress with dependency changes, so undisclosed phone-home behavior gets caught at the CI stage, before it reaches every developer's laptop.
  • Monitoring publisher and maintainer signals across registries, so account takeovers, unusual publishing patterns, or credential-phishing-driven pushes — the more classic nuget.org security breach scenario — get flagged alongside softer trust violations like this one.
  • Enforcing policy gates that block or require review for packages introducing new analyzers, post-install scripts, or other build-time execution paths, regardless of whether a formal CVE exists yet.

The lesson from Moq isn't that open source maintainers are untrustworthy — it's that trust needs continuous verification, not a one-time decision made when you first added a PackageReference. Safeguard is built to make that verification automatic, so your team finds out about the next SponsorLink-style surprise from a dashboard alert, not from a Hacker News thread.

Never miss an update

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