Container scanning generates false positives for .NET projects

Problem

Container scanning has two OSS engines under the hood, both of which can scan for vulnerabilities in .NET packages. While dogfooding this feature it was found that all of the results for our project are false positives due to incorrect implementations in both engines. During investigation it was found trivy adopted a similar implementation to grype's, both relying on a compiler output file ending in .deps.json.

Both engines implemented this feature based on an incorrect assumptions about the information provided in the .deps.json. Both tools assume this file is similar to a lock file in Python or a software bill of materials.

What is a .NET .deps.json file?

This file is generated during the build process for a modern .NET project. It captures the top-level dependencies used during the build process and related meta information about the top-level dependencies. It also captures top-level packages that are included in the .NET runtime and not included with the application itself.

The file contains some sections of interest to us:

  • targets -> runtime target (framework version) -> Our project (Peach.Web)
    • This section includes the build target's top-level dependencies. Top-level dependencies are the dependencies a developer has explicitly added to the project and doesn't include dependencies of dependencies that are implicitly included. This list includes dependencies that are included with the .NET runtime. For runtime packages, only the major/minor version is listed (3.1 vs. 3.1.28).
  • targets -> runtime target (framework version) -> Everything else
    • This includes the top-level dependencies dependency information. Of special note here, this is the meta data from the package itself, NOT what version of the implicitly installed dependencies were actually used/shipped.

A false positive journey

We can prove out our assumptions by tracing a path from a HIGH severity false positive to a top-level dependency.

Our journey starts with the pipeline output of a container scanning job for API Security:

Microsoft.AspNetCore.Mvc.DataAnnotations │ CVE-2017-0247       │ HIGH     │ 1.0.0             │ 1.1.3, 1.0.4        │ ASP.NET Core fails to properly 
validate web requests       │
│                                          │                     │          │                   │                     │ https://avd.aquasec.com/nvd/cve-2017-0247                  │

Here we see the package Microsoft.AspNetCore.Mvc.DataAnnotations is reported at v1.0.0 with the CVE dating back to 2017.

A quick check shows this package isn't included with our shipping code, so where is it coming from? It turns out this is a packages that is included with the .NET runtime and will always match the version of the runtime in use, in our case v3.1.28.

So how did this report get things so wrong? Let's take a look at the .deps.json file to see:

  1. First off we don't find Microsoft.AspNetCore.Mvc.DataAnnotations in the dependencies section of our project target in our .deps.json file. This tells us it's not a top-level dependency and is instead a dependency of a dependency.
  2. We do find an entry under targets with v1.0.0 listed.
      "Microsoft.AspNetCore.Mvc.DataAnnotations/1.0.0": {
        "dependencies": {
          "Microsoft.AspNetCore.Mvc.Core": "1.0.6",
          "Microsoft.Extensions.Localization": "1.0.0",
          "System.ComponentModel.Annotations": "4.7.0"
        }
      },
  1. Let's identify a path to this dependency from a top-level dependency:

    1. Microsoft.AspNetCore.Mvc.ViewFeatures depends on Microsoft.AspNetCore.Mvc.DataAnnotations
      "Microsoft.AspNetCore.Mvc.ViewFeatures/1.0.0": {
        "dependencies": {
          "Microsoft.AspNetCore.Antiforgery": "1.0.0",
          "Microsoft.AspNetCore.Diagnostics.Abstractions": "1.0.0",
          "Microsoft.AspNetCore.Html.Abstractions": "1.0.0",
          "Microsoft.AspNetCore.Mvc.Core": "1.0.6",
          "Microsoft.AspNetCore.Mvc.DataAnnotations": "1.0.0",
          "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.6",
          "Microsoft.Extensions.WebEncoders": "1.0.3",
          "Newtonsoft.Json": "12.0.3",
          "System.Buffers": "4.5.1",
          "System.Runtime.Serialization.Primitives": "4.3.0"
        }
      },
    2. BootstrapMvc.Mvc6 depends on Microsoft.AspNetCore.Mvc.ViewFeatures
      "BootstrapMvc.Mvc6/2.4.0": {
        "dependencies": {
          "BootstrapMvc.Core": "2.3.0",
          "Microsoft.AspNetCore.Html.Abstractions": "1.0.0",
          "Microsoft.AspNetCore.Mvc.ViewFeatures": "1.0.0",
          "Microsoft.AspNetCore.Razor": "1.0.0",
          "Microsoft.AspNetCore.Routing": "1.0.5"
        },
        "runtime": {
          "lib/netstandard1.6/BootstrapMvc.Mvc6.dll": {
            "assemblyVersion": "2.4.0.0",
            "fileVersion": "2.4.0.0"
          }
        },
        "compile": {
          "lib/netstandard1.6/BootstrapMvc.Mvc6.dll": {}
        }
      },
    3. BootstrapMvc.Bootstrap3Mvc6 depends on BootstrapMvc.Mvc6
      "BootstrapMvc.Bootstrap3Mvc6/2.4.1": {
        "dependencies": {
          "BootstrapMvc.Bootstrap3": "2.7.0",
          "BootstrapMvc.Mvc6": "2.4.0"
        },
        "runtime": {
          "lib/netstandard1.6/BootstrapMvc.Bootstrap3Mvc6.dll": {
            "assemblyVersion": "2.4.1.0",
            "fileVersion": "2.4.1.0"
          }
        },
        "compile": {
          "lib/netstandard1.6/BootstrapMvc.Bootstrap3Mvc6.dll": {}
        }
      },
  2. If we look closely at the BootstrapMvc.Mvc6 JSON entry, this is the first time we start seeing v1.0.0 packages. It looks to me like this is a minimum version, but how could we verify that? Let's take a look at nuget and see what it lists for the packages dependencies:

    image

    Well, what do you know? It matches! I think we can safely say these entries are just the packages dependency meta data, and not what was actually used during compilation or at runtime.

Where do we go from here?

It seems clear from our investigation that the .deps.json file isn't the right choice for container scanning. So what would a reasonable solution look like?

We need to reliably get a list of the exact packages in both the runtime and application code. One way to do this would be to build a database of file hashes for every version of every .NET package in nuget. This would allow collecting hashes of .NET assemblies and mapping them back to an exact package version. This resulting package version can then be checked for CVEs.

What are the limitations of this design?

.NET has several ways of packaging applications, including as a single executable that includes everything. This solution only works when all of the assets are on disk and viewable during inspection.

Edited Sep 02, 2022 by Michael Eddington
Assignee Loading
Time tracking Loading