Introduction
There’s been a lot of discussion recently around dependency confusion and supply chain based attack vectors. Most notably, this post outlines an effective campaign carried out at high scale. This post will cover some techniques for better managing your dependencies and ensuring you don’t fall victim to this type of attack.
Much like my other posts on software security, this will focus on rigor and discipline in your software development process, but can be assisted, and sometimes automated, by freely available tools. This post is going to cover Maven based Java projects, but the ideas and tooling have parallels in most major languages with first class dependency management tooling.
Getting a look at your dependency graph
In my previous post, I discussed some basic dependency management habits. I also split the refactoring into its own library. I’ll use this repository for the examples in this post, and all ideas discussed here will be represented in total there. When I approach dependency management the first thing I do is get an idea of how many dependencies a project has. We can do that in Maven using the following command:
mvn dependency:tree
Because this library is small it only contains a small handful of dependencies. The resulting graph paints an easy picture. Your project will be much different. If you have to work to count the number of dependencies in your project, it may be time to consider evaluating those needs. We will discuss pruning dependencies later in this post, but for now lets look through the results:
[INFO] com.aaronbedra:chronometrophobia:jar:1.1-SNAPSHOT
[INFO] +- commons-codec:commons-codec:jar:1.15:compile
[INFO] +- com.jnape.palatable:lambda:jar:5.3.0:compile
[INFO] +- com.jnape.palatable:shoki:jar:1.0-alpha-2:compile
[INFO] +- org.projectlombok:lombok:jar:1.18.18:provided
[INFO] +- junit:junit:jar:4.13.2:test
[INFO] | \- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] \- com.jnape.palatable:lambda:test-jar:tests:5.3.0:test
[INFO] ------------------------------------------------------------------------
The output tells us the project has 7 dependencies. In our pom.xml
,
we only define 6, but resolving the junit
dependency requires the
hamcrest-core
library, so that is added to our list. It’s just as
important to review the dependencies of your dependencies to make sure
you aren’t including anything that you don’t absolutely need to
complete your project.
Looking at this list there’s something we can correct. The shoki
dependency is only used in the project’s test suite, so we can scope
it as such. We can also see that the lombok
dependency is scoped
provided
to make sure it’s not exported in the resulting published
jar. A quick rerun of dependency:tree
results in the following:
[INFO] com.aaronbedra:chronometrophobia:jar:1.1-SNAPSHOT
[INFO] +- commons-codec:commons-codec:jar:1.15:compile
[INFO] +- com.jnape.palatable:lambda:jar:5.3.0:compile
[INFO] +- org.projectlombok:lombok:jar:1.18.18:provided
[INFO] +- com.jnape.palatable:shoki:jar:1.0-alpha-2:test
[INFO] +- junit:junit:jar:4.13.2:test
[INFO] | \- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] \- com.jnape.palatable:lambda:test-jar:tests:5.3.0:test
[INFO] ------------------------------------------------------------------------
We can now see that we are now down to 3 library dependencies and 4 test dependencies. We will revisit this list in a bit, but for now we have an understanding of what this project requires. If you are doing this exercise with your project and don’t know what a dependency does, take the time to find out, and make sure you have access to its source if possible so you can understand how it works.
Pinning versions
Maven is kind enough to let you know when you have specified a dependency without a version. You will see a message something like:
[WARNING] Some problems were encountered while building the effective model for com.aaronbedra:chronometrophobia:jar:1.1-SNAPSHOT
[WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-compiler-plugin is missing. @ line 116, column 21
This is a warning that should be eliminated from every build. There should be absolutely zero variance in what dependency is resolved when your dependency manager is run. It may be convenient to use version bands or not versions at all, but this practice results in unexpected behavior, bugs, CI problems, and a host of maintenance issues that are purely distractions. New versions of dependencies can also pull in new dependencies, which could introduce functionality you don’t intend to. If one of your dependencies accidentally falls victim to a dependency confusion attack, your project could transitively be impacted by just creating a new build or running your dependency manager. It’s best to lock your versions and only change them when necessary.
Checking for available updates
In order to understand if a project has dependencies or plugins with available updates, we can enlist the help of the Versions Maven Plugin.
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.8.1</version>
</plugin>
With this plugin you can run the following targets to get the information:
$ mvn versions:display-dependency-updates
[INFO] ------------------< com.aaronbedra:chronometrophobia >------------------
[INFO] Building chronometrophobia 1.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- versions-maven-plugin:2.8.1:display-dependency-updates (default-cli) @ chronometrophobia ---
[INFO] No dependencies in Dependency Management have newer versions.
[INFO]
[INFO] No dependencies in Dependencies have newer versions.
[INFO]
[INFO] ------------------------------------------------------------------------
$ mvn versions:display-plugin-updates
[INFO] The following plugin updates are available:
[INFO] maven-javadoc-plugin ............................... 3.1.1 -> 3.2.0
[INFO] maven-source-plugin ................................ 3.2.0 -> 3.2.1
[INFO] org.owasp:dependency-check-maven ................... 6.0.5 -> 6.1.1
[INFO] org.sonatype.plugins:nexus-staging-maven-plugin .... 1.6.3 -> 1.6.8
[INFO]
[WARNING] The following plugins do not have their version specified:
[WARNING] maven-clean-plugin ...................... (from super-pom) 3.1.0
[WARNING] maven-deploy-plugin .................. (from super-pom) 3.0.0-M1
[WARNING] maven-install-plugin ................. (from super-pom) 3.0.0-M1
[WARNING] maven-jar-plugin ........................ (from super-pom) 3.2.0
[WARNING] maven-resources-plugin .................. (from super-pom) 3.2.0
[WARNING] maven-site-plugin ....................... (from super-pom) 3.9.1
[WARNING] maven-surefire-plugin ................ (from super-pom) 3.0.0-M5
[INFO]
[INFO] Project inherits minimum Maven version as: 3.5.*
[INFO] Plugins require minimum Maven version of: 3.1.0
[INFO] Note: the super-pom from Maven 3.6.3 defines some of the plugin
[INFO] versions and may be influencing the plugins required minimum Maven
[INFO] version.
[INFO]
[INFO] No plugins require a newer version of Maven than specified by the pom.
[INFO]
[INFO] ------------------------------------------------------------------------
While this project’s direct dependencies are up to date, it looks like
several of the build plugins have available updates. Your build
plugins are still code that is executed within your project and they
should be treated with the same care as your other dependencies. A
couple quick version updates in the project’s pom.xml
will take care
of these notifications. This report also warns that there are
transitive plugin dependencies that do not have their versions
specified. This is because they aren’t specifically defined inside
pom.xml
. Our best course of action is to add each of them to the
<plugins>
section of our pom.xml
using the versions reported
above. This will allow us to keep these plugins up to date as well as
follow the version pinning rule presented above.
Dependency Convergence
Sometimes, your dependencies have dependencies. With popular dependencies used across many projects, a project can end up with multiple versions of the same dependency. Once you have multiple versions in your project, it may not be obvious which version actually gets used when your project is run. This can lead to difficult to debug issues and potentially re-exposure to vulnerable dependencies you may have thought you already eliminated. In the case of Maven, it will resolve all versions of all dependencies specified and include them. Luckily, the Maven Enforcer Plugin can help us shed some light on our issue.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M3</version>
<executions>
<execution>
<id>enforce-all</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<fail>true</fail>
<rules>
<requireMavenVersion>
<version>3.5.*</version>
</requireMavenVersion>
<requireJavaVersion>
<version>11</version>
</requireJavaVersion>
<dependencyConvergence />
</rules>
</configuration>
</execution>
</executions>
</plugin>
There are a few configuration options specified here, but the one we
really came for is <dependencyConvergence/>
. This option will cause
the plugin to report on all dependencies with multiple
versions. Combined with the <fail>true</fail>
setting above, it will
actually fail the build until resolved. This plugin is run as part of
the verify
target.
$ mvn verify
[WARNING]
Dependency convergence error for com.jnape.palatable:lambda:5.3.0 paths to dependency are:
+-com.aaronbedra:chronometrophobia:1.1-SNAPSHOT
+-com.jnape.palatable:lambda:5.3.0
and
+-com.aaronbedra:chronometrophobia:1.1-SNAPSHOT
+-com.jnape.palatable:shoki:1.0-alpha-2
+-com.jnape.palatable:lambda:5.2.0
and
+-com.aaronbedra:chronometrophobia:1.1-SNAPSHOT
+-com.jnape.palatable:lambda:5.3.0
[WARNING] Rule 2: org.apache.maven.plugins.enforcer.DependencyConvergence failed with message:
Failed while enforcing releasability. See above detailed error message.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
It looks like the shoki
plugin requires a version of lambda
older
than the one we require directly. Maven provides several ways to solve
this problem, but we will use <dependencyManagement>
to take care of
this particular issue:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.jnape.palatable</groupId>
<artifactId>lambda</artifactId>
<version>5.3.0</version>
</dependency>
</dependencies>
</dependencyManagement>
Explaining to Maven how to solve this makes our build successful again. There are quite a few other options available with this plugin, but dependency convergence is one of the important missing pieces of our puzzle.
Checking for Vulnerable Dependencies
Now that we have full control of our dependencies and how they are resolved, we need to make none of our dependencies have known vulnerabilities. We will use the OWASP Dependency Check Plugin accomplish this:
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>6.1.1</version>
<configuration>
<failBuildOnCVSS>1</failBuildOnCVSS>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
I prefer to set the failBuildOnCVSS
option to 1
. This forces
evaluation and explicit action on everything found. It is acceptable
to suppress a vulnerability if it does not impact your system, and
using the value of 1
here will force you to upgrade or
suppress. When suppression is chosen, be sure to add a detailed commit
message that explains your choice and why it should be suppressed
rather than updated. If you couple this with peer review, you can get
into a workflow where all suppressions have approval from another
person and there’s ample opportunity for discussion. Adding peer
approved suppression is just as important as adding the dependency
analysis tooling and should be considered an absolute requirement.
The first run of Dependency Check can take a while because of the CVE database updates. Now is a good time to get the first run out of the way so subsequent runs and execute faster.
Reducing Your Use of Dependencies
This is a more complicated subject. On one hand, pulling in a dependency to get the job done quickly has value. On the other, every dependency is a liability. My guideline for including a dependency is, if you can write it and fully test it in half a day or less, do that rather than include a dependency. This has several consequences:
- You end up with a collection of tooling that you can take with you between projects, or even package as a dependency
- You will learn more about fundamental concepts that will likely make you a better developer
- You will solve the problems you need to solve, and not try to make code written by someone else for something else work for you
- You will have explicit tests that prove the problem you are trying to solve is solved correctly
With all of this in mind, let’s revisit the dependencies of our project:
[INFO] com.aaronbedra:chronometrophobia:jar:1.1-SNAPSHOT
[INFO] +- commons-codec:commons-codec:jar:1.15:compile
[INFO] +- com.jnape.palatable:lambda:jar:5.3.0:compile
[INFO] +- com.jnape.palatable:shoki:jar:1.0-alpha-2:compile
[INFO] +- org.projectlombok:lombok:jar:1.18.18:provided
[INFO] +- junit:junit:jar:4.13.2:test
[INFO] | \- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] \- com.jnape.palatable:lambda:test-jar:tests:5.3.0:test
There’s not much here that isn’t absolutely necessary, but let’s take
a look at where commons-codec
is used:
public static ReaderT<SecureRandom, IO<?>, Seed> generateSeed(int length) {
return readerT(secureRandom -> io(() -> {
byte[] randomBytes = new byte[length];
secureRandom.nextBytes(randomBytes);
return seed(encodeHexString(randomBytes));
}));
}
The only use of this library is the encodeHexString
method. Encoding
a byte array to a hex string is a small enough exercise that it is a
perfect candidate for replacement. The result is
committed
to our library and is a significant reduction in overall code surface
with the commons-code
library removed.
It’s worth mentioning that this is unfortunately not a broadly accepted habit. We have seen entire ecosystems crumble because of dependencies on incredibly simple ideas.
Wrap-Up
With great power comes great responsibility. Dependency management and resolution is an incredibly difficult problem to solve. Most software projects will benefit from using dependencies. At the end of the day, if you choose to pull external dependencies into your software, you are assuming responsibility for each and every dependency until you choose to remove them. Using dependency managers is easy. Managing dependencies once you have added them requires care and diligence. As with most of my advice, this is rooted in rigor and discipline, which will continue to be the cornerstone of any good Software Security practice.