Squatting npm for Remote Code Execution

2024-08-01

Similar to Alex Birsan's "dependency confusion" bug, npm's CLI tool npx allows for RCE via binary confusion.

This is an intentional feature, but in my opinion has surprising negative results.

Background

npx, which is distributed as part of the npm CLI tool, and is a wrapper for npm exec allows convenient execution of arbitrary commands in local npm packages, and packages fetched from the NPM registry.

package.json's .bin property "is a map of command name to local file name".

If npx is run for a command not listed in locally-available node modules, npx will prompt to install required packages.

Where things go Wrong

If a command is not available locally, there is a mismatch of expectations:

This mismatch can be leveraged by an attacker to trick users into installing and running unexpected code.

Proof of concept

As a proof-of-concept, I registered various packages with names matching bin scripts in popular packages, which list their own bin commands. These bin commands are a benign wrapper which attempts to run the script the user would have expected.

A malicious actor could replace these benign scripts with anything they want (e.g. data exfiltration, modification, destruction; system shutdown etc.).

Example 1

I have npm package pm2-runtime.

User interaction example:

$ npx pm2-runtime
Need to install the following packages:
pm2-runtime@5.4.1
Ok to proceed? (y) 
...

Example 2

I registered npm package vue-cli-service.

Impact

Remote code execution on developers' computers, CI environments, or production environments.

According to current statistics on npmjs.com, last week:

As these packages have 0 dependents, it's likely that most of these downloads represent users of npx or npm exec downloading and executing these packages' bin scripts.

I have some other similar packages with negligible download numbers, which also imply that the majority of the download counts are for real users rather than npm package scanners etc.

Is it a bug?

I submitted this to npm's bug bounty program, and received the reply:

Can you please clarify how this can be used by an attacker in a form that isn't typo-squatting or requiring social engineering of a victim to run arbitrary commands?

Typo Squatting

I don't think this is typo squatting - the npx commands are typed correctly, and depending on circumstances will execute expected, non-malicious code.

Social Engineering

Social engineering is not required - here are some examples that can be seen in the vue.js documentation at https://cli.vuejs.org/guide/cli-service.html#checking-all-available-commands:

I did not maliciously encouraged anyone to download or install demonstration packages I uploaded to npmjs.com.

As can be seen from the download numbers for demo packages, many legitimate users are accepting (or not being shown) the install prompt for the malicious package.

package weekly downloads
vue-cli-service 996
pm2-runtime 567
cross-env-shell 143
run-many 132
typeorm-ts-node-commonjs 118
typeorm-ts-node-esm 57
node-waf 42
ts-node-script 36
sentry-prune-profiler-binaries 22
sucrase-node 21

User Prompt

Trust

While it's true that in some circumstances npx will prompt the user before installing the malicious package, the name of the package will appear correct, as it exactly matches the expected binary script name:

$ npx vue-cli-service
Need to install the following packages:
vue-cli-service@5.0.10
Ok to proceed? (y)

As in the above example, the version number can also be set to match the shadowed package, adding a further layer of credibility.

Without a good knowledge of the underlying workings of npm exec/npx, an unsuspecting user is likely to be misled into executing code from the malicious package.

Skipped Prompt

Under certain circumstances, this confirmation step is skipped. Specifically:

  1. if one of various environment variables are set (e.g. CI, XCS, VELA, DRONE, etc.), or
  2. if npx is running in a non-TTY environment

This behaviour is defined in the latest version of the latest branch of the npm source code at https://github.com/npm/cli/blob/4e81a6a4106e4e125b0eefda042b75cfae0a5f23/workspaces/libnpmexec/lib/index.js#L276-L288

This can be tested manually like so:

$ CI=1 npx vue-cli-service
npm WARN exec The following package was not found and will be installed: vue-cli-service@5.0.10

or

$ DRONE=1 npx vue-cli-service
npm WARN exec The following package was not found and will be installed: vue-cli-service@5.0.10

Final Response

The bug was closed as Informative with the response:

The behavior outlined in this report is consistent with documented expectations for how npx works and interacts with both packages and commands. As such, this report will not be eligible for reward under our Bug Bounty program.

As an additional protection, npm implements automated malware detection to help secure users against installation of malicious packages.

As of today, the squatted packages are still available on npmjs.com.

Related