Dependency confusion is a hot topic right now thanks to Alex Birsan’s excellent work in this area, and like many people, you may be wondering how you can protect yourself from these attacks.
As a quick recap, a dependency confusion attack is a type of supply chain attack against projects that have a mixture of public and private dependencies. In the dependency confusion attack, an attacker registers a package with the same name as one of your private dependencies on the public repository for your package manager. If your configuration is not secure, your package manager ends up downloading the package with the highest version number regardless of which repository it came from, resulting in attacker-controlled code running on your systems.
For example, let’s say your project at your company Acme Inc. depends on the public package “rspec” and also the private package “acme-logger”, and it is configured to fetch dependencies from both the public package repository and your internal private repository:
# INSECURE! DO NOT DO THIS! source "https://rubygems.org" source "https://our-private-packages.acme.example" gem "rspec" gem "acme-logger"
“acme-logger” is only published in your private repository and is at version 1.1.
If an attacker publishes their own “acme-logger” on the public repository with version 1.2, what happens? The next time the dependencies are updated, the attacker’s package is pulled in, and you are now running their code.
% bundle update [DEPRECATED] Your Gemfile contains multiple primary sources. Using `source` more than once without a block is a security risk, and may result in installing unexpected gems. To resolve this warning, use a block to indicate which gems should come from the secondary source. To upgrade this warning to an error, run `bundle config set disable_multisource true`. Fetching gem metadata from https://our-private-packages.acme.example/. Fetching gem metadata from https://rubygems.org/. Using rspec 3.10.0 Fetching acme-logger 1.2.0 (was 1.1.0)
Your goal, then, is to ensure that your private packages can only be pulled from your private repository and never from the public one. The exact methods to do this will vary from package manager to package manager, but you should be able to achieve some level of assurance by reading the documentation and making sure your configuration is correct.
Typically, the basic step is to ensure that your private packages are only ever pulled from your private repository, and there’s usually some way to make sure that is the case. For example, with Bundler, you can specify which source to pull a dependency from:
source "https://our-private-packages.acme.example" do gem "acme-logger" end
But what if you want more protection?
Maybe your package manager doesn’t allow you to do this (for example, the latest version of Bundler, at the time of writing, is still vulnerable to dependency confusion attacks). Maybe it does, but you have identified dependency confusion as a particular concern in your threat model, and you don’t want to depend entirely on your package manager. Dependency resolution is an inherently complicated problem after all, and bugs in dependency resolution algorithms are not unheard of.
Either way, you want an extra backstop against dependency confusion.
With some package managers — or repositories, really, as this is a feature of repositories — you can use namespacing. The feature might have a different name in your package manager, but many support it. It essentially boils down to reserving an area of the repository just for you. No-one else can publish packages inside your namespace, so as long as you consistently put all your private packages in your namespace, no-one except you can ever publish a public package with the same name.
Namespacing is great because it’s a really robust way to avoid dependency confusion attacks. As a primary method it’s very solid, and it can also be used as additional protection in a belt-and-braces1 approach. But what if your package manager doesn’t have namespacing of any kind? For example, Ruby’s Bundler and Rust’s Cargo both have completely flat namespaces.
Introducing Virtual Namespacing
Virtual namespacing is a technique you can use to simulate namespace support in any package manager without modifying the package manager itself, but by changing the repositories instead.
Let’s say you operate two repositories at Acme Inc.,
our-private-packages. As the names suggest, the former is a read-through cache of the public repository, and the latter is for your private packages.
Assuming that your tools only ever pull from these two repositories and never from the public repository directly (we’ll come back to this), you can completely defeat dependency confusion attacks by ensuring your private dependencies are never pulled from the public repo mirror repository.
You could do this by excluding the names of all your private packages from being served by the public mirror, but you would need to keep that list up to date at all times. Instead, there is a way to ensure this for all packages, present and future, without having to update the configuration when new packages are added.
You need to do two things: restrict which packages can be downloaded from the public mirror repository, and which packages can be uploaded to the private package repository.
To ensure that all packages are covered, without having to keep a list up to date, you need to pick a consistent naming scheme for all your private packages. This naming scheme will then effectively form a namespace for your packages. A simple scheme is to add a prefix, for example, “acme-“. Note that whatever scheme you pick, it needs to be simple enough to be enforceable in the configuration of your repositories, and unlikely to clash with any legitimate public packages you might need to import. “facebook-“ could be a bad prefix, for example, because people might publish libraries to interact with Facebook that clash with that naming scheme.
Now that you have a naming scheme, you create a virtual namespace by configuring two policies on your repositories:
public-repo-mirrorwill refuse to serve any package whose name starts with the prefix “acme-“ (this is an “exclude pattern” in Artifactory).
our-private-packageswill only accept uploaded packages if their names start with “acme-“ (an “include pattern” in Artifactory).
This is a hard stop for dependency confusion attacks, because any package allowed to be uploaded to your private repo cannot be pulled from the public repo. If your package manager already defends you against these attacks, you now have a bullet-proof backstop if it ever fails. If it doesn’t, this provides you with a perfectly adequate first line of defence.
In practice, there are a couple of details you need to pay attention to as well: enforcing that only these repositories are used, and handling existing dependencies that don’t match your “namespace” pattern.
If you’re setting up a private repository and creating private dependencies for the first time, you can skip this.
However, if you already have a collection of private dependencies and they don’t all already follow your new namespaced naming pattern, applying the steps above will break everything, as your private package repository will no longer serve them.
What you need to do is add each and every existing dependency to the “exclude” list of the public mirror repository, and to the “include” list of the private repository. If it’s a long list, you probably want to automate this (or at least automate checking it) to ensure you don’t leave any gaps — if you miss a single dependency from the exclude list of the public mirror, you’re potentially still vulnerable to a dependency confusion attack!
However, you only need to do this once. You don’t need to keep updating the exclude or include lists, because all new dependencies you create will match the prefix pattern.
Because your namespace only exists within your repositories, and isn’t being enforced by your package manager or the public repository, it’s critical that you ensure dependencies are only ever fetched from the repositories under your control.
For your production servers, arguably the most important area to get this right, the solution is also the easiest. As long as you’re building your servers (or server images, or container images, or what have you) via some automation, you can configure the package manager to only use your repositories in your builds/images.
Additionally, you can have some automated check such as a lint ensuring that the dependency manifest in each of your applications is using the right repositories.
Developer machines are more challenging. As well as installing via the dependency manifest, developers sometimes install dependencies directly on the command-line. This is the difference between
bundle install and
gem install our-awesome-library, as an example. This is harder to lock down, because by default the package manager will go out to the public repository.
If you have a way of controlling the configuration of developer machines, such as Jamf or Puppet, you could use it to configure the default repository for the package manager.
If you don’t, you could consider blocking direct access to the public repository in your firewall. (Don’t forget to exclude your public repository mirror from the rule!)
To recap, these are the steps you need to create your virtual namespace:
- Pick a consistent naming scheme for private packages, such as a prefix.
- Block any packages matching the naming scheme from being pulled from your public repository mirror.
- Block any packages not matching the naming scheme from being published to your private repository.
- Add existing dependencies that do not match the naming scheme to the block and allow lists.
- Ensure all your machines only talk to your repositories, and never to the public repository directly.
Whether you’re looking to add a second line of defence against dependency confusion attacks, or desperately searching for a first line while your package manager fixes its bugs, I hope this was helpful.
Thanks to Jim Rollenhagen for telling me about this technique in the first place and inspiring this post, and thank you to Agnieszka and my anonymous reviewers for their valuable feedback on drafts of this post.
Or belt-and-suspenders, if you’re American. An approach that is redundant, like using both a belt and braces (or suspenders) to keep one’s trousers up. ↩