Some text here

Stick a Pin in It: Managing Dependencies for Supply Chain Security

Managing software dependencies is an important part of software supply chain security. Here are three approaches you can take to pin your dependencies to known-good versions.

Ben Cotton

January 23, 2025

Some software supply chain attacks involve a bad actor targeting a specific organization. Most, however, are more opportunistic — the bad guys compromise a commonly-used tool or service and take advantage of whatever victims they can find. One popular and effective approach is to compromise an open source tool or library. Fortunately, this can also be easy to defend against.

Modern software applications are not fully self-contained — they build on existing libraries to provide basic functionality, leaving developers free to focus on the differentiated parts of the application. These existing libraries are built on other libraries in turn, which means the total dependency graph can include dozens of dependencies. On the one hand, this means there's a lot of code that the end developer doesn't need to write. On the other hand, a vulnerability in any of those dependencies can compromise the application.

How does the security-conscious developer guard against supply chain attacks against their dependencies? By “pinning” the dependencies: specifying a particular version of the dependency to use.

Pinning strategies

There are different approaches to pinning and, like everything in security, come with tradeoffs. The implementation of each of these approaches will vary by platform or ecosystem.

Branch/series pinning

The simplest form of pinning is pinning to a branch or release series. For example, you might have a GitHub Action pinned to “example/neat-action@stable” which pins to the “stable” branch of the neat-action repository. Or in a Ruby Gemfile, you’d specify gem 'nifty-library', '~> 1.3.0', which would use the latest bugfix release in the nifty-library 1.3 release series.

The advantage of this approach is that you get the latest version, which may have fixes for vulnerabilities and other bugs, on every build. It doesn’t require paying close attention to all of your upstreams, which makes it pretty scalable. But it also means that you leave yourself open to the risk that later releases are bad in some way. It could be that a bad actor inserted malware into that release, or that a vulnerability was accidentally introduced, or even just that a change was incompatible with your application.

Tag/release pinning

A stricter approach is to pin the dependency to a specific release number or git tag. The GitHub Action example looks like example/neat-action@v7.6.5 and the Ruby example is gem 'nifty-library', '1.3.12'. This solves the problem of problems introduced in subsequent releases because you don’t use those releases until you explicitly choose to. But it doesn’t solve that problem fully. Git tags can be overwritten. If a bad actor takes over your dependency’s repo, they can update the v7.6.5 tag to point to a vulnerable release instead of the safe one you had been using.

While it’s safer than the branch/series pinning approach, it loses the “you get updates without having to work for them” benefit. With tag/release pinning, you have to keep track of your dependencies releases yourself. This means if you’re not paying attention, you might miss out on the announcement of — and fixes for — newly-discovered vulnerabilities.

Hash pinning

The strictest approach is to pin the dependency based on a hash, like a git commit hash or a checksum. In a GitHub Action, you’d specify example/neat-action@cf67f7de07a40608e1a3371186dc22c74083c462. Ruby’s Bundler command recently added support for checksums, which adds a section in the Gemfile.lock file that looks like nifty-library (1.3.12) sha256=b409c96b0acc90abe6aa8fd9656eaff0980c1b36c9e22b8f7c490a46eafc2204.

Although accidental and intentional hash collisions are theoretically possible, they are very difficult to pull off. The real-world risk for this sort of attack is very low. Pinning by hash is very effective at preventing software supply chain attacks, but it also requires active monitoring of your dependencies in the same way that tag/release pinning does.

Choosing your strategy

The pinning strategy that you pick will depend on your risk tolerance and your ability to keep up with your dependency graph. Across, or even within, your projects, you may choose different strategies. For example, you might pin your application dependencies by hash, but your GitHub Actions by version.

In general, the more risk-averse you are, the stricter the approach needs to be. Conversely, if you don’t have the bandwidth to keep up with all of your dependencies, you might go for a more relaxed approach, understanding that vulnerability fixes are more likely than vulnerability creation within a release series. If your dependencies come from trusted sources that have strong security practices, you may feel more comfortable using a looser strategy.

Of course, the more you can automate your software supply chain, the more you can trust the security of dependencies. Open source tools like GUAC and commercial offerings like the Kusari Platform provide you with updated information about vulnerabilities and other issues across your entire application portfolio. This makes managing software supply chains of any size easier and more predictable.

Like what you read? Share it with others.

Other blog posts 

The latest industry news, interviews, technologies, and resources.

View all posts

Previous

No older posts

Next

No newer posts

Want to learn more?

Book a Demo
By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.