07 Feb, 2019

Third-Party Library Dependencies

by Sean Lyford

It is very easy to focus on what is directly presented when discussing outdated and potentially vulnerable software. Depending on the library or framework, we have to design and develop around their unique behaviors. The potential problem that seems to catch people off-guard is that our project dependencies may have dependencies of their own which will have their own outdated and vulnerable variants.

This blog post does not necessarily describe a wide-spread problem. It does not outline an impressive new attack vector nor does it provide specifics that all developers must strictly adhere to. Instead, this blog post should be taken as a call-to-action to analyze the full list of dependencies used in projects and for any vulnerabilities introduced by our imported code.

The Problem

Hopefully, it should come as no surprise that many software projects depend on code written by other authors. These third-party libraries are maintained and iterated upon just as we do with our own individual projects. And, just as we do with our own, these dependencies are declared end-of-life and may even contain vulnerabilities.

Despite the nature of dependencies being commonplace, the discovery of vulnerable code indirectly included within projects seems to catch some off guard. A part of nVisium’s typical process is to assess libraries and services for known vulnerabilities, such as CVEs. Though I cannot describe in detail, I will say that I have had conversations with clients resulting in mild confusion when discussing discovered CVEs. These CVEs were associated with libraries pulled during the installation of other dependencies. As a result, the developers were unsure why the discussion focused on libraries of which they had no knowledge.

A more explicit and well-known example occurred towards the end of 2018. September 9th of that year, malicious code was injected into the Event-Stream NodeJS module and targeted a Bitcoin wallet called Copay. Naturally, once this was discovered, a scramble occurred to determine what existing software may have included the compromised module. As an example, Visual Studio Code stated that they were unaffected but did disable extensions from the VS Code Marketplace that did include the compromised library.

To further compound the impact of indirectly included dependencies, the infamous Left-Pad incident suddenly broke many nightly builds and many projects were left unable to build their projects. A notable example was Babel, a JavaScript compiler. Though NPM does not contain the tagged version of Babel from the time of the incident, version 7.2.2 of Babel fresh from NPM does not directly include Left-Pad. That honor belongs to Jsdom, which is imported via Jest-Environment-Jsdom. The actual Left-Pad library is about 6 levels from Babel-Core within the dependency tree and would still result in the project not building without its inclusion.

Despite focusing on NPM packages, this is not a problem unique to NPM. For example, PHP Pear faced a serious compromise just recently, meaning that compromised packages may occur at any time. Additionally, the catastrophic Equifax breach is believed to be associated with a vulnerable version of Apache Struts. I am not advocating for Not In-House (NIH) Syndrome as reusing code from other parties can prove invaluable.

The Blockers

There are few options available to us under the assumption that a vulnerable library is detected. As a starting point, determining if the application is vulnerable in the first place may prove difficult for several reasons. First, not all publicly disclosed vulnerabilities include details or a proof-of-concept. Second, many detailed vulnerabilities are unique to subsets of functionality, meaning that only certain implementations will exhibit the flaws necessary. Third, we may be limited by resources to fully determine the impact or likelihood of such flaws.

With these potential restrictions, it is best to assume that the dependency may be successfully attacked. From this point, we are left with two options. First, update our explicit dependency, the one our code directly calls, that indirectly or directly pulls the vulnerable component. With any luck, a more recent update will no longer pull vulnerable versions. The more extreme option is to manually update libraries, resulting in upstream changes. The latter may be necessary but is undesirable as more work must be done during future updates. The former may also prove difficult, especially as technical debt builds.

Another outcome that may potentially derail any development is an end-of-life statement or simply the lack of any recent updates. Project versions and even entire projects eventually end, resulting in unmaintained code. In such situations, we must decide to either move on or maintain the code ourselves. If the former, finding a suitable alternative may take time and may bring large rewrites even in the best of cases. If the latter, we have increased our burden that may lead to more technical debt and strained resources.

Determining if projects are no longer supported can also be a tedious problem to solve. Official end-of-life statements may not be publicly well-known, existing only in single blog posts on project or author blogs. These statements can easily be ignored or may be difficult to find if one does not know exactly what to look for. Additionally, projects may go silently unsupported as authors simply move on and no longer perform maintenance. Determining if the library is now dead will rely on an individual judgement call based on factors such as open support tickets and the dates of last activity. Neither situation may be quick and easy to assess and should be carefully considered.


To sidetrack for a moment, I wish to put emphasis on the practice of dating packages. Just because dates are recent does not mean that all imported code is up to date.

First, repositories do not always have the most recent versions of libraries. Babel, which was referenced in the previous section, is version 7.2.2 within NPM and the most recent tagged release is 7.3.1 on Github at the time of writing.

Second, repository descriptions may not align with actual releases. As one example that drove my desire for this post, I noticed something very odd within a RichFaces UI Maven repository: all the dates for the versions were incorrect. RichFaces 3.3.4 released in July 2011, RichFaces 4.0.0 released in March 2011, and RichFaces 3.3.3 launched sometime in 2009. Additionally, all versions through 3.3.4 are vulnerable to CVE-2018-14667. When browsing the Maven repository for org.richfaces.ui, Maven will give a variety of repositories, dates, and versions. If one needed RichFaces and were unfamiliar with Maven repositories, additional work would be necessary to ensure that the versions and deployments are up to date.

Note: Despite the focus on RichFaces, it should be recognized that RichFaces has been end-of-life since February of 2016.

Dated versions within Maven

Third, recently updated projects may still have older, more vulnerable libraries. As a rather weak example, the Spring PetClinic is a test project used to show Spring Boot in action. Despite receiving updates months prior, a vulnerable version of Jackson Databind was imported, as flagged by OWASP’s Dependency Check. However, as I wrote this post, another update was provided and this issue no longer persists.

Vulnerability report header for Spring PetClinic

The Resolution

Ultimately, what is needed is more discovery. We must first understand what is imported with every project build. The following is a list of commands and utilities that show the full dependency tree for libraries in various languages:

  • Java Maven: mvn dependency:tree
  • Ruby Bundler: Gemfile.lock
    • gem dependency does not appear to present a full tree
  • NodeJS NPM: npm-remote-ls (unofficial)
  • NodeJS Yarn: yarn list
  • Python Pip: pipdeptree (unofficial)
  • Go: go list -f {{.Deps}} (naturally, no versions)

The above is only a subset and, if there is a repository or dependency-fetching system, there is more likely a standard or third-party tool that can assist in printing dependency trees for manual inspection.

Additionally, applications should have dependencies tested for known vulnerabilities. Note that all testing should be done with built projects. Simply scraping the list of explicit dependencies will miss vulnerabilities deeper in the dependency tree. Some tools, such as NPM, include an audit command that will print known vulnerabilities (npm audit). Additionally, some open source tooling exists that can scan built projects, such as OWASP Dependency Check. Enterprise solutions do exist, such as Sonatype, but come with a price tag.

Conclusion

What runs within our applications is not limited to just our internal code but our dependencies and any further libraries imported by them. However, we must not neglect to ensure that these deeper dependencies are up to date and secure. Additionally, we must take care when trusting repositories as they may lag behind or even contain incorrect or compromised data. Please take this post not as a specific learning point but as a reminder to audit and recall the whole of our applications.

Thank you for reading.