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:
- a module's
package.json
's.bin
property allows a map of commands with names which do not necessarily match the module's name npx
will prompt to install the package whose name matches the requested command- in some cases, there is no prompt and the package is installed automatically (see below)
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
- Expected package:
pm2
- Weekly downloads: 2.5 million
- bin scripts:
pm2
pm2-dev
pm2-docker
pm2-runtime
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
- Expected package:
@vue/cli-service
- Weekly downloads: 600,000
- bin script:
vue-cli-service
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:
pm2-runtime
was downloaded 637 timesvue-cli-service
was downloaded 1081 times
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 |
Running totals
Updated: 2024-10-28
All package total: 76,446 downloads
Top 10 packages
package | total downloads |
---|---|
pm2-runtime |
23,184 |
vue-cli-service |
20,831 |
typeorm-ts-node-commonjs |
8,322 |
cross-env-shell |
5,422 |
typeorm-ts-node-esm |
3,591 |
sentry-expo-upload-sourcemaps |
3,461 |
run-many |
3,400 |
node-waf |
1,486 |
ts-node-script |
844 |
sentry-upload-sourcemaps |
503 |
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:
- if one of various environment variables are set (e.g.
CI
,XCS
,VELA
,DRONE
, etc.), or - 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.