injecting malicious module to npm package

Why npm lockfiles can be a security blindspot for injecting malicious modules

| By Liran Tal

I recently started playing around with the idea of threat modeling packages on the npm ecosystem. Can an event-stream incident happen again? How about other supply chain attacks? What will be the next vector of attack that we haven’t seen yet and might it be entirely preventable?

And then, one day I had a eureka! 🎉 Let me show you how easy it is to introduce back doors that are easily missed by project owners… leaving your code insecure.

The following picture shows how I injected a malicious package through a pull request I made on an open source package on npm. 

Take a closer look and see if you can spot it:

yarn lockfile diff on github

Now, when I go through my pull request review checklist for this PR, this is how it looks: 

  • The packages I introduced are known in the ecosystem and are vulnerabilities free – check.
  • There are no typosquatting attempts in these package names – check.
  • These are valid versions of those packages and aren’t malicious in and of themselves – check.

Looks good to me. Let’s go ahead and merge!

But wait. Did you carefully review that lockfile? Of course not, and I don’t blame you. It’s not at all obvious, considering these restraints:

  • GitHub’s interface defaults to folding diffs whenever they are larger than a couple hundred lines of code in length.
  • What’s the point in reviewing this file? It’s machine-generated and it’s also not very reader-friendly.


Regardless of these restraints, what could go wrong really?

Let’s expand the diff and take a look at what’s inside:

yarn lockfile metadata resolvers

When a lockfile is present, whether that is Yarn’s yarn.lock or npm’s package-lock.json, an install will consult the lockfile as the primary source of truth for package versions and their sources for dependencies installation.

See what I meant before? A 500-line change on a lockfile is actually a pretty low number. I’ve seen thousands of line changes applied to an npm package-lock.json or a Yarn yarn.lock file. I mean, who really reviews thousands of lines of character changes? To make matters worse, in this lockfile we’ve seen dependency version changes and metadata updates– nothing really out of the ordinary all in all.

If we scroll a bit further down to review the rest of the changes applied to this file we find this gem:

yarn lockfile shows injection of malicious npm module

What becomes clear when you look closer, is that I replaced the original ms npm package to resolve it with my own version, which is stored in my GitHub repository. I should have gotten it from the official npm registry, just as was originally set in the lockfile of the project.

This project uses a few different semvers of ms, while I only changed ms@2.1.1 to use my own customized rendition:

yarn list --pattern ms
yarn list v1.17.3
├─ @babel/helper-module-transforms@7.4.4
├─ debug@2.6.9
│  └─ ms@2.0.0
├─ express@4.17.1
│  └─ ms@2.1.1
├─ ms@2.1.2
├─ send@0.16.2
│  └─ ms@2.0.0
├─ serve-static@1.14.1
│  └─ ms@2.1.1
✨  Done in 0.49s.

It’s pretty hard to remember to review on such a granular level – and it’s no wonder so few actually review lockfile changes character-by-character! 

So, what does my own version of ms@2.1.1 do?

When this pull request gets merged, I inject my malicious version of ms@2.1.1 into the code in order to control its behavior during runtime.

In this way, I could introduce a backdoor, alter the logic of the ms module or I could run some postinstall scripts. Additionally, the hosting URL at https://github.com/lirantal/ms/tarball/master is obviously alarming for this experiment but would it be easy for you to spot it if I were to buy a typosquatting domain to host it under https://registry.npmgs.org/ms/ ?

For the sake of this experiment, my ms-hosted package has a simple postinstall script that outputs some text and writes to a file so that if you were to install the package itself, you could then confirm that the post-install event simply executes upon a yarn install:

"scripts": {
   "postinstall": "echo im installed && echo hello > /tmp/world.txt",
}

For project-based applications, lockfile security is critical and is susceptible to the type of malicious injection attacks as this article outlines.

Lockfiles being used in packages as libraries however, are solely designed for use within the development workflow of the package itself. For that reason, when the project (package) is installed as a dependency in another project, its lockfile isn’t used. This means that lockfile injection would primarily compromise and risk project maintainers and collaborators as far as libraries are concerned.

What can we do to increase lockfile security?

Because lockfiles are meant to be read by machines it means that they are easy to process automatically. That makes it the perfect job for a linter and some preset policies!

As a countermeasure against the above attack vector, I built lockfile-lint to help with this task. I added the file as another short step of the static code analysis that you can run on a CI server such as Travis CI or Circle CI.

At the very least, you should validate all resources that are served:

  • over HTTPS
  • from a trusted source

As an example, to run those checks in CI for a Yarn project, it’s as simple as:

$ npx lockfile-lint --path yarn.lock --type yarn --validate-https --allowed-hosts yarnpkg.org

Here’s an example showing common errors detected while linting a project’s lockfile:

lockfile lint example for npm project

Summary

Attacks can manifest in many different forms and types… even from lockfiles!

With the event-stream incident we’ve seen the first successful launch of a social engineering attack on open source project maintainers. This was perhaps one of the most sophisticated attacks we’ve seen on software supply-chain security in the npm and JavaScript ecosystem to date.

A lockfile injection could attack in a similar fashion, and so you should have proper practices in place to mitigate this issue, or at least reduce the risk. Consider the following:

  • Carefully review changes to lockfiles (lockfile-lint can help with that).
  • Allow lockfile changes from core maintainers and not sporadic contributors.
  • Avoid the use of lockfiles entirely for libraries.

If you’re interested to read more about lockfiles and how they are used, I wrote in depth about making sense of package lockfiles in the npm ecosystem.