socket fix (NuGet): packages pruned by the .NET 10 SDK are reported as vulnerable; fixes are useless and misreported

Ouverte
#1,675 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

Personne n'a encore pris cette issue.

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
48/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
Active
Stack technique
typescript
Domaine
cli, security

Piste de recherche

Start with the NuGet dependency graph handling used by socket fix, using the attached minimal net10.0 project and its obj/project.assets.json or packages.lock.json as the reproduction. Check how the CLI decides which packages are upgrade candidates and applies fixes after restore, including the CPM and lock file variants. Done means SDK-pruned packages are not reported or modified, and the four variants produce the expected clean result.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

Summary

On a net10.0 project, socket fix reports pkg:nuget/[email protected] (GHSA-8g4q-xg66-9fp4) and tries to fix it. That package is not part of the resolved NuGet graph: since the .NET 10 SDK, NuGet package pruning (PrunePackageReference) is enabled by default and removes packages provided by the directly referenced shared frameworks (here System.Text.Json <= 10.0.x, provided by Microsoft.NETCore.App) from the graph.

socket fix then writes a change that has no effect (or a useless direct reference), and reports the outcome inconsistently depending on the project setup. Four minimal variants are attached; all of them reproduce the false positive:

Variant (attached zip) Change made by socket fix Reported outcome Exit code
cpm-lock (CPM + packages.lock.json) Useless <PackageVersion Include="system.text.json" Version="8.0.5" /> system.text.json is locked at no version after the upgrade to 8.0.5, Coana command failed (exit code 2); change not reverted 1
cpm-no-lock (CPM, no lock file) Same useless PackageVersion 1/1 workspaces were upgraded successfully, Finished! 0
no-cpm-lock (single .csproj + lock file) <PackageReference Include="system.text.json" Version="8.0.5" /> + lock file entry; NU1510 on every restore Locked system.text.json 8.0.5, 2/2 workspaces were upgraded successfully, Finished! 0
no-cpm-no-lock (single .csproj) Same direct PackageReference; NU1510 on every restore 1/1 workspaces were upgraded successfully, Finished! 0

The simplest reproduction is no-cpm-no-lock: a single .csproj.

Environment

Component Version
Socket CLI 1.12.1
Coana CLI (bundled) 15.13.3
OS WSL2, Debian GNU/Linux 12 (bookworm), kernel 5.15.133.1-microsoft-standard-WSL2
Node.js / npm v23.11.1 / 10.9.2
.NET SDK 10.0.403 (Microsoft.NETCore.App 10.0.14)
global.json none (pinning the SDK does not change the result: tested with SDK 10.0.113 and 10.0.401, both prune System.Text.Json for net10.0)

Attachments

Simplest variant (no-cpm-no-lock), Repro.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.1" />
  </ItemGroup>
</Project>

CPM variants, Directory.Packages.props (the .csproj then has <PackageReference Include="Microsoft.Extensions.DependencyModel" /> without a version):

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
    <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="8.0.1" />
  </ItemGroup>
</Project>

"lock" variants add <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> and ship the packages.lock.json generated by dotnet restore. It contains only Microsoft.Extensions.DependencyModel 8.0.1, no System.Text.Json.

Steps to reproduce

for v in cpm-lock cpm-no-lock no-cpm-lock no-cpm-no-lock; do
  d=socket-nuget-prune-repro-$v
  unzip -q "$d.zip" && cd "$d"
  git init -q && git add -A && git commit -qm repro   # only to see the diff afterwards
  socket fix . --all
  echo "$v: exit=$?"
  git --no-pager diff
  cd ..
done

Expected

GHSA-8g4q-xg66-9fp4 is not reported (or is reported as not affecting the project), because System.Text.Json is not in the resolved graph of a net10.0 project. The advisory affects the System.Text.Json packages 6.0.0-6.0.9 and 8.0.0-8.0.4, and only lists .NET 6 and .NET 8 runtimes as affected; the System.Text.Json 10.0.x shipped with any .NET 10 runtime is not affected. No file is modified. Exit code 0.

Actual

All variants start with:

info: Upgrading purls for .../socket-nuget-prune-repro-<variant>:
        pkg:nuget/[email protected] → 8.0.5

cpm-lock:

debug: Running command: dotnet restore .../Repro.csproj --force-evaluate -p:RestoreLockedMode=false -p:NuGetAudit=false
info: ✅ Patch applied ─ Directory.Packages.props
info: ❌ system.text.json is locked at no version after the upgrade to 8.0.5 ─ packages.lock.json
debug: Error stack: UpgradePurlPartialError: Some workspaces failed to upgrade
1/2 workspaces were upgraded successfully.
✖  Coana command failed (exit code 2): command failed

Exit code 1. packages.lock.json unchanged; the working tree keeps:

     <PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="8.0.1" />
+    <PackageVersion Include="system.text.json" Version="8.0.5" />

cpm-no-lock: same diff, ✅ Patch applied ─ Directory.Packages.props, 1/1 workspaces were upgraded successfully., ✔ Finished!, exit code 0.

no-cpm-lock:

info: ✅ Patch applied ─ Repro.csproj
info: ✅ Locked system.text.json 8.0.5 ─ packages.lock.json
2/2 workspaces were upgraded successfully.
✔ Finished!

Exit code 0. Repro.csproj gains <PackageReference Include="system.text.json" Version="8.0.5" /> and packages.lock.json gains System.Text.Json 8.0.5 as a direct dependency. Every subsequent restore emits:

warning NU1510: PackageReference system.text.json will not be pruned. This package is automatically available and does not need to be referenced explicitly. Remove the PackageReference item.

no-cpm-no-lock: same PackageReference added, 1/1 workspaces were upgraded successfully., ✔ Finished!, exit code 0, same NU1510 warning afterwards.

Why the advisory is a false positive

  • Microsoft.Extensions.DependencyModel 8.0.1 declares System.Text.Json >= 8.0.4 in its net8.0 dependency group (the nearest to net10.0).
  • The .NET 10 SDK prunes it: obj/project.assets.json lists "System.Text.Json": "(,10.0.32767]" under packagesToPrune.
  • dotnet nuget why Repro.csproj System.Text.Json prints Project 'Repro' does not have a dependency on 'System.Text.Json'.
  • The lock file (when enabled) does not contain System.Text.Json.
  • At compile time and runtime, System.Text.Json comes from the shared framework (Microsoft.NETCore.App 10.0.x), not from a NuGet package.
  • Microsoft documentation: package pruning "is enabled by default for all frameworks of a project that targets >= .NET 10.0 in the .NET 10 SDK", and one of its stated goals is the "reduction of false positives by component scanners". https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#prunepackagereference

The prune list depends on the target framework and on the shared frameworks the project references directly. On an empty net10.0 project, packagesToPrune has 272 entries (Microsoft.NETCore.App). With <FrameworkReference Include="Microsoft.AspNetCore.App" /> (or the Web SDK) it has 412 entries, and also covers Microsoft.Extensions.* and Microsoft.AspNetCore.* up to 10.0.x. ASP.NET Core projects are therefore exposed to the same false positives on many more packages.

With <RestoreEnablePackagePruning>false</RestoreEnablePackagePruning> (the pre-.NET 10 default), System.Text.Json 8.0.4 does appear in the graph and the lock file, and pinning 8.0.5 does update the lock file. Socket's analysis matches that graph, not the default .NET 10 one. Even then, the package assembly is not used: the build picks the framework's System.Text.Json 10.0.x and does not copy the package DLL to the output.

The SDK-resolved, pruned graph is already available on disk: packages.lock.json (committed) and obj/project.assets.json (after restore) both contain the full dependency graph with edges. The Socket docs state that lock files are "fully supported and provide the best accuracy", yet the reported dependency is absent from the lock file. The graph appears to be rebuilt from nuspec dependency groups without applying pruning.

Issues

  1. False positive on pruned packages (root cause). Packages pruned by the .NET SDK are treated as dependencies in all four variants.
  2. CPM + lock file: failed fix not reverted. The upgrade is reported as failed (locked at no version), but the Directory.Packages.props change stays in the working tree. Coana 15.11.6 added a rollback when dotnet restore fails; here restore succeeds and only the lock check fails, so nothing is undone.
  3. CPM without lock file: silent false success. No post-restore check runs, so a change with no effect is reported as a successful fix (exit 0). The advisory will be reported again on the next scan.
  4. Without CPM: useless direct reference reported as a fix. A direct PackageReference to a framework-provided package is added and reported as successful (exit 0, with or without lock file). It triggers NU1510 on every restore, which fails builds using TreatWarningsAsErrors.
  5. No CLI workaround. No option makes the analysis honor the target framework or pruning (--dynamic-sbom-inference only covers Gradle, sbt and Maven; socket manifest has no dotnet subcommand). Disabling pruning through the environment does not work either (see "Attempted workaround").

Real-world impact

On a 48-project net10.0 solution (CPM + lock files), the advisory is triggered by Microsoft.CodeAnalysis.CSharp.Features 4.12.0 (pulled in by a third-party SDK package), which declares System.Text.Json 8.0.4 in its older-framework dependency groups. socket fix pins System.Text.Json in Directory.Packages.props, restores all 48 projects with --force-evaluate, and fails on all of them with locked at no version: projects that would get System.Text.Json transitively do not, because it is pruned and the central pin has no effect (this issue); the other projects never depended on it and are only checked because they share Directory.Packages.props (#1674). The modified Directory.Packages.props is left in place.

Attempted workaround: disabling pruning

Since Socket's analysis matches the unpruned graph, we tried running socket fix with pruning disabled through the environment (MSBuild reads environment variables as properties, and Coana's dotnet restore inherits them):

RestoreEnablePackagePruning=false socket fix . --all --no-major-updates --ecosystems nuget

On the same 48-project solution, this gets further but is still blocked:

  • Projects that really get System.Text.Json without pruning are now reported as fixed (Locked system.text.json ...).
  • Test projects first fail with NU1109: Detected package downgrade: System.Text.Json from 10.0.10 to centrally defined 8.0.5 (Microsoft.Testing.Extensions.CodeCoverage 18.11.2 requires System.Text.Json >= 10.0.10). Coana handles it (Raised to resolve a package downgrade) and pins 10.0.10 instead.
  • 22 projects still fail with system.text.json is locked at no version after the upgrade: they never depend on System.Text.Json, even unpruned, but they import the modified Directory.Packages.props and are checked anyway (#1674).
  • Result: 27/49 workspaces were upgraded successfully, Coana command failed (exit code 2).

Even if it fully succeeded, this would not be an acceptable workaround:

  • every lock file is rewritten with all the packages the SDK normally prunes (here +2,145 lines across 49 files), and they diverge from any normal restore (local or CI) unless pruning is disabled permanently;
  • it pins System.Text.Json (a framework-provided package) in Directory.Packages.props, while the build still uses the framework's 10.0.x assembly;
  • it reintroduces the false positives that pruning removes for every other scanner, including NuGetAudit.

Suggested fix

  • Check before fixing: use the graph the SDK actually resolved.
    • What changed in .NET 10: NuGet package pruning (PrunePackageReference) was introduced as opt-in in .NET SDK 9.0.200 (NuGet 6.13), for transitive packages only. The .NET 10 SDK enables it by default for all frameworks of a project that targets net10.0 or later, and extends it to direct packages. Packages provided by the directly referenced shared frameworks (Microsoft.NETCore.App, plus Microsoft.AspNetCore.App for web projects) are removed from the graph at restore time, up to the framework version (see packagesToPrune above). Pruned transitive packages are not downloaded and do not appear in any NuGet output (project.assets.json targets, packages.lock.json). Before pruning, they stayed in the graph and the build's conflict resolution picked the framework assembly instead.
    • Build the NuGet graph from what the SDK resolved: packages.lock.json when present, otherwise obj/project.assets.json after restore (targets and packagesToPrune), rather than re-resolving from nuspec dependency groups. Without those files, apply the pruning rules of the target framework (not of the SDK installed where the tool runs) and of the directly referenced shared frameworks.
    • If the vulnerable version is not in the resolved graph, report the advisory as "not affected" and do not modify any file. Writing anything is useless or harmful: a PackageVersion for a pruned package has no effect, and a direct PackageReference stays in the graph and lock file but its assets are not used (NuGet applies IncludeAssets="none"), and triggers NU1510 ("does not need to be referenced explicitly. Remove the PackageReference item"), which breaks builds using TreatWarningsAsErrors.
    • This also matters for #1674: once only the absence of the vulnerable version is checked after restore, a pruned package would otherwise pass that check and be reported as a successful fix (see the root-cause comment in #1674: switching NuGet to requireLocked: false alone would have exactly this effect).
  • Revert consistently.
    • When a fix fails, or cannot be shown to change the resolved version (in the lock file, or in obj/project.assets.json when there is no lock file), revert the manifest and lock file changes, report the fix as not applied, and exit with a non-zero code.
    • Today the outcome depends on the project setup: with CPM and a lock file, the failure is reported but Directory.Packages.props stays modified; with CPM and no lock file, a change with no effect is reported as a success (exit 0). Poetry fixes are already reverted when they fail.
Langage dominant
TypeScript
Étoiles
335
Forks
74
Merge moyen
1 j 11 h
PR mergées (30 j)
86

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de SocketDev/socket-cli

Toutes les issues de SocketDev/socket-cli

Issues similaires

Plus d'issues TypeScript

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.