Considering our reliance on open source and third party components, it’s nearly impossible to estimate how many open source libraries we’re using, especially with dependency management tools that pull in third party dependencies automatically.
Adding to the challenge of keeping track of the open source components that make up our codebase, is the tangled web of transitive dependencies. These are the libraries that your dependencies are linked to — your dependencies’ dependencies. Transitive dependencies are particularly hard to track, as are their frequent updates by automated package managers. That’s where lockfiles come in.
Your package manager’s lockfile is one of the things that define your application. It contains all of the records about the exact versions of external packages and dependencies that were installed. This helps ensure that when you want to run the same project on another machine, the exact same versions of the external dependencies will be installed.
Without a lockfile, running a command like “npm install” will produce different results over time. This means that two developers might experience different behaviors from different versions of dependencies, but the issues don’t stop there. It could even cause problems such as production dependencies not matching what was tested and approved earlier by automated and/or manual testing.
In addition to helping control versioning, package lockfiles can also be used to generate a simple SBOM of your external dependencies.
Unfortunately, despite their name, lockfiles are not always fully locked, and there are also supply chain security risks associated with using them. We’ve assessed that this behavior is present in at least a few package managers, including Yarn and Bundler.
It might surprise you that in some cases, running the install command might actually trigger an update, and even install different packages than the ones specified in the lockfile. This poses a certain risk to your supply chain, since it means that you have less visibility and control over the ecosystem you’re operating in.
The exact conditions in which a re-install is triggered vary between package managers. For Bundler it may be a new platform, and for Yarn and npm it may be caused by package.json and yarn.lock going out of sync. If the dependency tree in the package lock does not satisfy the dependencies listed in the package.json, npm will update the package lock.
Ruby packages can have varying versions per-platform, with completely different dependencies. Lockfiles only apply to platforms that were added to them. When the bundle install command is executed on a new platform, it tries to find packages specific to the new platform whose version matches the one locked for other platforms. Those packages will be preferred if found, and they may come with their own transitive dependencies that differ from the ones defined for other platforms.
In the cases of NPM and Yarn, it is a bit different. When a package.json and yarn.lock go out of sync, whether it is triggered by developer accidental actions or misconfigured CI tools, “npm-install” will do its best to satisfy the requirements, but if it cannot, it will add / change the lockfile and the underlying dependencies.
In a few words: a bit of chaos and unpredictability. Open source software supply chains are as strong as their weakest link. A well crafted attack could target specific vulnerable platforms, skipping others to hide itself.
In addition to the security risks, lockfile inconsistencies could also add friction to the software development process. Since the dependencies differ, their behavior may vary as well. The result: something that works on the CI, may not work on production unless platforms are compatible.
This behavior can also hurt the validity of your inventory reporting and create confusion when generating and analyzing an SBOM. When open source components are used only in a particular step in the SDLC, they might completely bypass the lockfile declarations, and be overlooked, resulting in an incomplete bill of materials and open source components that remain un-managed.
The mitigation approach is similar for all the package managers. The installation needs to happen with the indication of the lockfile being frozen. For bundler that is done by using the “–frozen” flag or by setting it using the “bundle config set –local frozen ‘true’” command.
With that flag, during an install, Bundler won’t allow the Gemfile.lock to be updated. It will exit with a non-zero status in case anything would have to be changed.
With Yarn, equivalent behavior can be achieved by using the “–frozen-lockfile” flag.
Below is an example of how the install behaviour will differ depending on this flag usage:
Regular bundle install outcome
Frozen bundle install outcome
As you can see, when a given platform was not included in the lockfile, an error is printed and the install process stops. This behaviour ensures that there are no ambiguities during the install process. You end up with a lockfile that will always be either installed exactly as expected or an error message will be presented.
As always, whenever security is involved, you can never be too careful. Not having reproducibility of the installation process of your dependencies may lead to both legal and security problems, especially since they may go unnoticed
That’s why it’s important to implement all the tools in your toolbox to both ensure complete visibility, and prevent security threats.
Learn more about ensuring your open source supply chain security.