DEV Community

Srujan
Srujan

Posted on

Escaping a transitive dependency nightmare with some help from Gradle

Every so often at work, our CI pipeline's dependency check job spits out another vulnerable dependency detected in our application. Not entirely surprising considering the rate at which new CVEs are published these days.

This isn't much of a problem for direct (or first-level) dependencies, as long as there's a patch for it out already. All you do is upgrade your dependency to the latest patched version, ensure the update doesn't actually break your application, and you're good to go.

If the vulnerability lies in a transitive dependency (a dependency of another dependency, typically one that you don't directly declare), it starts to get a little messy. A couple of issues can arise here:

  1. You don't know where this dependency even came from.
  2. The parent dependency hasn't updated to the latest patched version yet.

Luckily, modern build tools help make this chore (an important chore nonetheless) a little less painful. In our case, this was with some help from Gradle.

Tracking down transitive dependencies

Identifying the top-level dependency where a vulnerable dependency is used is usually the first step to patching your application.

Gradle's build scan can probably provide the most information about your project in one central place, but this involves publishing build details to Gradle's servers, something you might want to avoid if you don't want sensitive details related to your project out in public.

Instead, a fast and easy way to do this locally on the command-line is with the dependencyInsight task provided by Gradle. This visualizes the origin of a transitive dependency and, in the case of multiple conflicting versions, also shows why a particular version was chosen.

The latter is what separates it from another similar task called dependencies which only provides a raw visualization of every dependency in your project.

You can run this task from the command-line as follows:

$ gradle -q dependencyInsight --dependency <dependency-name>
Enter fullscreen mode Exit fullscreen mode

You can also provide an option --configuration to specify the gradle configuration to scan. If nothing is provided, it scans compileClasspath by default.

In my case, I wanted to track down commons-io (version 2.6 had the vulnerability CVE-2021-29425) in my project. Running the above gave the following output:

commons-io:commons-io:2.6 (selected by rule)
   variant "compile" [
      org.gradle.status              = release (not requested)
      org.gradle.usage               = java-api
      org.gradle.libraryelements     = jar (compatible with: classes+resources)
      org.gradle.category            = library

      Requested attributes not found in the selected variant:
         org.gradle.dependency.bundling = external
         org.gradle.jvm.version         = 11
   ]

commons-io:commons-io:2.6 
+--- <internal-library>:0.1.372
     +--- compileClasspath
Enter fullscreen mode Exit fullscreen mode

(Simplified output only for representation, most large projects have much more complex dependency trees)

This tells me that commons-io v2.6 is a dependency of an internal library that we used between teams. Now I could have waited for the team responsible for maintaining this internal library to patch it themselves, but that would mean blocking my CI pipeline from further deployments for an unknown period of time. Instead, I need a way to force-upgrade this dependency to a patched version until the maintainers patch their library as well.

This is where another feature of Gradle comes into play.

Applying constraints on transitive dependencies

Dependency constraints is the easiest (and the recommended) way to force resolve a particular version of a transitive dependency. Constraints apply on all direct and transitive dependencies defined in a project.

A constraint on a dependency can be applied in your build.gradle script like this:

dependencies {
    implementation 'org.apache.httpcomponents:httpclient'
    constraints {
        implementation('org.apache.httpcomponents:httpclient:4.5.3') {
            because 'previous versions have a bug impacting this application'
        }
        implementation('commons-codec:commons-codec:1.11') {
            because 'version 1.9 pulled from httpclient has bugs affecting this application'
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The above constraints force Gradle to always use httpclient v4.5.3 and forces resolution of the commons-codec library to v1.11 instead of v1.9 specified in httpclient.

Therefore, to force upgrade to a patched version of commons-io, I had to add the following constraint in my build.gradle script:

constraints {
        implementation('commons-io:commons-io:2.10.0') {
            because('Versions < 2.7 allows limited path traversal with FileNameUtils.normalize method - CVE-2021-29425')
        }
    }
Enter fullscreen mode Exit fullscreen mode

The because closure allows you to explain why this version should be chosen and it will appear in the dependencyInsight output.

Now my dependencyInsight output looks like this:

commons-io:commons-io:2.10.0
   variant "compile" [
      org.gradle.status              = release (not requested)
      org.gradle.usage               = java-api
      org.gradle.libraryelements     = jar (compatible with: classes+resources)
      org.gradle.category            = library

      Requested attributes not found in the selected variant:
         org.gradle.dependency.bundling = external
         org.gradle.jvm.version         = 11
   ]
   Selection reasons:
      - By constraint : Versions < 2.7 allows limited path traversal with FileNameUtils.normalize method - CVE-2021-29425


commons-io:commons-io:2.6 -> 2.10.0
+--- <internal-library>:0.1.372
     +--- compileClasspath
Enter fullscreen mode Exit fullscreen mode

Now I can rest assured that the vulnerable dependency always resolves to the patched version instead of the one declared inside the internal library.

Discussion (2)

Collapse
prenagha profile image
Padraic Renaghan

Any pointers on good options for the CI dependency check job would be appreciated
And is there a Gradle plugin/task that can check Gradle dependencies against the CVE database?
Thanks for any pointers

Collapse
srujan_g profile image
Srujan Author

We use OWASP dependency-check in our CI pipeline. It's also available as a Gradle plugin and a standalone CLI tool.

owasp.org/www-project-dependency-c...