Part of the beauty of modern software creation is that it’s very easy to obtain third-party dependencies when building and deploying an application thanks to the use of open source language package managers. Need a Node package? Just use the package manager npm to download and install it on demand. Want a Python module? Pip, Python’s package manager, will include it in your software environment in seconds.
When third-party dependencies are combined with internally developed dependencies, the stage is set for systems to potentially become “confused” over which dependency to use. In this case, if you fail to validate the source of the packages, you can easily fall victim to a dependency confusion attack in which attackers trick your applications into running malicious, external dependencies instead of the internal ones you intend.
Dependency confusion substitutes malicious third-party code for a legitimate internal dependency. There are various approaches to creating a dependency confusion attack, including:
- Namespacing – by uploading a malicious package to a public repository that is named similar to a trusted, internally-used package, systems that omit a namespace check will mistakenly pull in the malicious package.
- DNS Spoofing – by using a technique like DNS spoofing, systems can be directed to pull dependencies from malicious repositories while displaying what looks like legitimate URLs/paths.
- Scripting – by modifying build/install scripts or CI/CD configurations, systems can be tricked into downloading dependencies from a malicious source.
A typical dependency confusion attack might look like:
- A bad actor discovers the name of a dependency used internally by some organization to develop their software application.
- The bad actor creates a similar dependency, embeds malware, names it the same as the internal dependency and renumbers it to higher version than the one used internally.
- The bad actor uploads the compromised dependency to a public repository.
- The next time the package manager requests the specific dependency, it may pull the compromised dependency from the public repository rather than the local repository.
While you can’t avoid dependencies, you can be strategic about how you solve and verify the ones you use in your applications. There are a number of best practices and tools – including pre-built runtime environments, one of the most effective anti-dependency-confusion resources you can use to avoid dependency confusion risks.
Dependency Confusion Software Supply Chain Attacks
Dependency confusion was voted PortSwigger’s #1 Web Hacking Technique in 2021 due to its ubiquity, since it can be extended to any open source programming language, and has been proven effective at targeting high-profile companies like Apple and Microsoft.
In fact, since Alex Birsan’s original post about dependency confusion in February 2021, the threat has only grown. Researchers have found hundreds of confusing NPM packages as well as numerous real-world confusion attacks:
The risk of compromise by dependency confusion is increased by:
- Implicit Trust in Public Repositories – far too many organizations continue to blindly trust open source ecosystems despite the fact that they provide no security guarantees.
- Difficulty of Detection – there is simply no means of scanning for dependency confusion problems until a compromised dependency has already been included in your software.
Luckily, dependency confusion doesn’t lead to fundamentally new types of security risks. However, it does represent a new, never-before-seen vector of attack that security-conscious organizations will need to counter.
Best Practices for Mitigating Dependency Confusion Risks
So, what can you do to prevent dependency confusion exploits? There’s no simple answer, but a variety of practices can help:
- Utilize Scopes/Namespaces – some package managers allow the use of namespaces, IDs or other prefixes, which you can use to ensure that internal dependencies be pulled from private repositories.
- Secure Your CI/CD Environment – you can mitigate the risk that attackers insert malicious dependency paths in your build scripts or CI/CD configurations by ensuring that your CI/CD environment is locked down. Implementing a secure build service is a key step to ensuring builds aren’t subject to dependency confusion.
- Validate Checksums – wherever possible, validate dependency downloads by ensuring checksums match those documented on official package sources. This can be difficult to automate with changing dependencies/versions, but again, setting up a secure build service can help.
- Vendor Your Dependencies – rather than pulling dependencies from private or public repositories on demand every time you build an environment, you can reduce the risk of dependency confusion by embedding the source code for all of your dependencies in your code repository. This means you only need to validate a single URL, but dependency vendoring can be complex.
Alternatively, consider using a secure, pre-built runtime environment offered by trusted vendors like ActiveState. In this case, all dependencies are built by the vendor and packaged into a runtime suitable for deployment to your development, test, production and other environments. Pre-built runtime environments make dependency confusion attacks the responsibility of the vendor, but rather than blindly trusting the vendor, ensure their build process includes:
- Scripted Builds – build scripts that cannot be accessed and modified within the build service, preventing exploits.
- Ephemeral, Isolated Build Steps – every step in a build process executes in its own container, which is discarded at the completion of each step. In other words, containers are purpose-built to perform a single function, reducing the potential for compromise.
- Hermetic Environments – containers have no internet access, preventing (for example) dynamic packages from including remote resources.
Like dependency vendoring, using a pre-built environment such as that generated by the ActiveState Platform means you only need to validate a single URL when creating an environment. And using the ActiveState Platform also means you can avoid all the complexity of dependency vendoring, as well.
All modern software is built with dependencies, which means they are all susceptible to dependency confusion attacks. While no silver bullet currently exists to eliminate the threat, implementing URL and checksum validation best practices can help reduce the risk.
Adopting dependency management strategies like dependency vendoring or prebuilt runtime environments can also help by limiting the number of URLs you need to verify. Additionally, prebuilt runtime environments can offer further benefits beyond just security, including:
- Faster container build times since you don’t need to wait for dependencies to be individually downloaded and resolved.
- Greater environment consistency by limiting configuration drift through a single, central runtime environment from which can be programmatically derived branches for development, test and production environments, as well as the entire CI/CD pipeline.
- Simpler runtime deployment, requiring only a single command to install and/or update the target environment.