Lodash is an absolute titan in the JavaScript world. First released in 2012 as a fork of the popular Underscore.js, it quickly became a go-to utility library for developers everywhere. Even though the last version was released five years ago, it still boasts over 65 million downloads a week! This just goes to show that some technologies, even after a decade, are so reliable that developers just keep coming back to them, even though all but the very latest version have at least one high-severity vulnerability.
But here's a little secret about the tech world: sometimes, those older, trusty versions can harbor some unexpected quirks. We recently had a client who needed a sealed version of Lodash 3.10.1, a version that's over ten years old! In order to fulfill this request, we had to dive deep into the library's old build process, a complex and manual affair involving a custom tool called lodash-cli
. What we discovered was a fascinating anomaly that highlights a common pitfall in open-source development.
The NPM release of Lodash 3.10.1 contains two different versions of the library, each intended for a different use case: the "modern" build and the "modularized" build. The "modern" build is the entire Lodash library in one file (require('lodash')
), while the "modularized" build lets you import only the specific functions you need (require('lodash/lang/clone')
), which helps keep your app's file size small. Logic would suggest that both variants should be consistent, but our investigation revealed something completely different.
Turns out, the "modern" build actually contained the code from the previous version, 3.10.0, while the "modularized" build had the code from the correct 3.10.1 release. This created a bug that depended on how the library was imported.
To illustrate the issue, let's look at a concrete example. According to the changelog, version 3.10.1 ensured that the _.clone function
"provides the correct number of arguments to customizer." We can easily see this in action by running the following code:
var clone1 = require('lodash').clone;
var clone2 = require('lodash/lang/clone');
var customizer = function () {
console.log('customizer args:', arguments.length);
};
clone1({}, false, customizer, {});
clone2({}, false, customizer, {});
This code produces the following surprising output:
customizer args: 1
customizer args: 3
The first line, using the "modern" build (imported via require('lodash')
), shows that the old bug still exists, providing only one argument to the customizer function. The second line, which uses the "modularized" build (imported via require('lodash/lang/clone')
), shows the corrected behavior from version 3.10.1, passing three arguments.
So, why the discrepancy? The inconsistency arose because the index.js
file, which forms the "modern" build, was created from the older 3.10.0 source code. In contrast, the modular file lang/clone.js was built from the newer 3.10.1 source. This discrepancy likely occurred because the build tool, lodash-cli
, has a circular dependency on Lodash itself, which probably caused the mix-up during the build process.
While this particular bug isn't a major security issue, it serves as a powerful reminder of how small mistakes can lead to big headaches. We've seen similar issues in other open-source projects. For example, a fix for CVE-2023-36665 in the protobuf.js
library was applied to the source code but wasn't included in the minimized version of the code in the dist
folder that was shipped with the NPM package. This forced the maintainers to release a new version just to fix the distribution problem.
The lessons here are clear:
This little deep dive into Lodash's history shows that even the most trusted libraries can have hidden secrets. So next time you're about to npm install
, remember that sometimes, what you see isn't always what you get.