Ken Muse

The Hidden Dangers in Dependencies


Dependencies are complex things. Most times, we just thing of them as packages that provide one or more features we need for our code. We know that we need to be careful with that code to ensure it doesn’t have any security exploits. At the same time, we rarely stop to consider the full power of those packages. Because of that, we can miss important security risks.

NPM (JavaScript/Typescript)

Let’s take Node Package Manager (NPM) as an example. As far back as 2016, the NPM blog recommended add --ignore-scripts to npm install when installing an untrusted package (either manually or by updating .npmrc). Why? Because npm packages support running scripts when they are installed. NPM’s script support is a feature that some packages (such as Husky) use to configure the environment properly.

With npm, scripts can be included in the package and can hook into lifecycle events. For example:

  • prepare - Normally run before the code is packaged for distribution. When a package is installed directly from a Git repository, this hook is run after package’s dependencies are installed. This is because the code is downloaded and then packaged prior to installing it. This can also run when you call `npm install`` without arguments. Husky depends on this event to ensure that its setup scripts get re-run automatically whenever you restore your dependencies.
  • preinstall, install, postinstall (and others) - These scripts are intended to be run automatically whenever a package is installed. Using install scripts is now frowned upon except in specific architecture-specific situations. Instead, most packages requiring architecture-specific native code compilations should rely on a binding.gyp file. If this file exists in the root of the package and there’s no install script, npm will call node-gyp rebuild.

When was the last time you audited third-party packages to understand their use of scripts?

Of course, this isn’t the only way that you can be affected by a malicious package. Packages can fall victim to a “dependency confusion attack” (Snyk has a great write-up on this exploit). Another exploit takes advantage of the fact that most companies fail to properly use scopes for internal packages. A malicious actor can register a public package with the same name as a company’s internal package to take advantage of this mistake. If you’re using an internal “proxy” registry to shadow the public registry, it can open you up to further hijack exploits. See GitHub’s blog post on this topic.

Most teams don’t consider the risks from their practices (or their use of internal registries). They also lack the time to check and validate the full dependency tree created by the packages they use. This is why having automated security tools is essential!

NuGet (.NET)

Security challenges exist with all package management systems. For example, NuGet supports creating packages that contain an install script: tools/init.ps1. This script can be automatically executed by Visual Studio. Earlier this year, JFRog posted about the trend of init.ps1 exploits. Older versions of Visual Studio and .NET supported scripts for package lifecycle events – install.ps1 and uninstall.ps1. If you’re running older tools, then you may have additional exposure!

NuGet packages can also contribute to the build process for .NET. A package can contribute additional settings, tasks, and steps. Some features of .NET depend on this functionality. For example, SourceLink support is implemented as tasks that execute as part of the build process. Because of this, it’s important to make sure you trust the vendors providing your dependencies.

Maven (Java)

Think Java is immune? Think again! A few years ago, proof of concept attacks were published demonstrating “repository injection”. If a package declares custom repositories, those repositories can be used to resolve its dependencies. Once a given package name is resolved and downloaded, other requests for the same package will resolve to the local copy. This allowed malicious packages a path for hijacking dependencies.

Maven also supports lifecycle events and has an extensible plugin model. As a build engine, it also supports code execution. Consequently, Maven warns that the “security model assumes you trust the pom.xml and the code, dependencies, and repositories that are used in your build.”

Like using NuGet, Maven also expects that you understand your dependencies and what they might be contributing to both your code and your build.

Care and feeding

These aren’t the only package management systems or the only risks. The nature of package management always carries with it security considerations. Dependencies require management. They are trusted as part of your build process, part of your deployed codebase, or both. Consequently, it’s important for you to continuously understand your supply chain and the components within it. Your supply chain gets direct access to developer systems and production servers, so it must be treated with due respect.

If you’re implementing self-hosted systems for managing and deploying dependencies, you need to also understand how an additional layer may alter your security posture. Being able to centralize and manage upstream dependencies can make it easier to get visibility into your supply chain. At the same time, the mixing of internal and external package sources can create additional security considerations.

In short, dependencies and supply chains need ongoing care and feeding. Without that, you may quickly find yourself getting bitten!