
CVE-2026-44789 — n8n <1.123.43 Requête HTTP : pollution de prototype par pagination menant à RCE (NODE_OPTIONS runner-spawn gadget). Laboratoire + PoC automatisé, vérifié e2e.
Un créateur de workflow n8n
< 1.123.43authentifié pollueObject.prototypedans le processus serveur n8n via les paramètres de pagination du nœud HTTP Request, puis escalade vers l'exécution de code à distance en abusant de la façon dont n8n lance son exécuteur de tâches — fuyant unNODE_OPTIONSpollué dans un processusnodeenfant et contournant le sandbox du nœud Code.
| CVE | CVE-2026-44789 |
| Avis | GHSA-c8xv-5998-g76h |
| Affecté | < 1.123.43, 2.0.0-rc.0 … < 2.20.7, 2.21.0 … < 2.22.1 |
| Corrigé | 1.123.43 / 2.20.7 / 2.22.1 |
| Classe | CWE-1321 (Pollution de prototype) → CWE-94 (RCE) |
| CVSS | 9.4 (CVSS 4.0) / Critique |
| Auth | Authentifié (permission de création/modification de workflow) |
| Statut | CONFIRMÉ — chaîne complète reproduite de bout en bout contre n8nio/n8n:1.123.42 |
L'avis public indique seulement que la pollution "combinée à d'autres techniques pourrait conduire à une RCE" — il ne divulgue pas de gadget. Ce dépôt documente et automatise une chaîne concrète et vérifiée de pollution de prototype → RCE.
packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts, mode de pagination
updateAParameterInEachRequest :
paginationData.request[parameter.type]![parameterName] = parameterValue;
parameter.type, parameterName et parameterValue proviennent tous du JSON
du workflow (contrôlé par l'attaquant). Avec parameter.type = "__proto__",
paginationData.request["__proto__"] résout Object.prototype, donc l'affectation
écrit Object.prototype[parameterName] = parameterValue — une pollution de prototype
globale dans le processus serveur n8n.
Le correctif utilise Object.create(null) pour paginationData.request, de sorte que
["__proto__"] soit une clé ordinaire (null-proto) au lieu du prototype.
packages/cli/src/task-runners/task-runner-process-js.ts lance l'exécuteur de tâches JS :
return spawn('node', [...flags, startScript], { env: this.getProcessEnvVars(...) });
normalizeSpawnArguments de Node construit l'environnement enfant avec
for (const key in env), qui énumère les propriétés énumérables héritées.
Polluer Object.prototype.NODE_OPTIONS = "--require=/path/evil.js" se transmet donc
dans l'environnement de l'exécuteur lancé. L'enfant est node, qui honore
NODE_OPTIONS, donc il --require le fichier de l'attaquant au démarrage → exécution
de code en dehors du sandbox du nœud Code, en tant qu'utilisateur du service n8n.
L'exécuteur est lancé au démarrage, mais son cycle de vie le relance à chaque fois que
le processus se termine (onProcessExit → start()). L'attaquant force un redémarrage après
la pollution en bloquant l'exécuteur avec un nœud Code (timeout de tâche / OOM le tue).
exploit.py)1. écrire /tmp/evil.js via les nœuds Set → Convert to File → Read/Write Files
2. bloquer l'exécuteur de tâches via un nœud Code ( while(true){} ) [AVANT la pollution]
3. polluer Object.prototype.NODE_OPTIONS via le nœud HTTP Request ( type="__proto__" )
4. l'exécuteur bloqué expire → le processus principal relance 'node' → hérite de NODE_OPTIONS
→ require('/tmp/evil.js') → RCE
L'ordre est important : la pollution fait de NODE_OPTIONS une clé énumérable propre-less
sur Object.prototype, que la couche TypeORM de n8n rencontre (for…in sur les entités),
brisant la persistance du workflow. Donc l'exécuteur doit déjà être bloqué avant que
la pollution n'arrive ; le redémarrage récupère alors l'environnement pollué.
Périmètre de configuration (lisez d'abord). La primitive de pollution de prototype se déclenche sur toute version affectée sans changement de configuration. Le gadget RCE nécessite l'exécuteur de tâches, qui est atteint différemment selon la branche :
- n8n 2.x affecté (2.0.0–2.20.6, 2.21.0–2.22.0) : les exécuteurs de tâches sont par défaut/obligatoires (
N8N_RUNNERS_ENABLEDest déprécié →SAFE_TO_REMOVE), donc la chaîne RCE complète est configuration par défaut.- n8n 1.123.x affecté (cette image de laboratoire) : les exécuteurs de tâches sont par défaut désactivés, donc le laboratoire définit
N8N_RUNNERS_ENABLED=truepour refléter la valeur par défaut de la 2.x.N8N_RUNNERS_TASK_TIMEOUTest abaissé uniquement pour accélérer le redémarrage de l'exécuteur bloqué — ce n'est pas nécessaire pour le bogue.
docker compose -f lab/docker-compose.yml up -d # n8nio/n8n:1.123.42, exécuteurs activés pour refléter 2.x
python3 exploit.py http://127.0.0.1:5678 -c "id; hostname"
# la sortie de la commande apparaît sur l'hôte n8n :
docker compose -f lab/docker-compose.yml exec n8n cat /tmp/n8n_rce_proof
# RCE uid=1000(node) gid=1000(node) groups=1000(node)
# <nom_hôte>
exploit.py n'utilise que la bibliothèque standard Python et pilote l'API REST n8n
de bout en bout (configuration du propriétaire/connexion → déploiement des workflows → déclenchement de la chaîne).
Observé :
[*] étape 1 : écriture de la charge utile --require dans /tmp/evil.js (via le nœud Read/Write Files)
[*] étape 2 : envoi d'une tâche bloquante -> l'exécuteur est maintenant occupé
[*] étape 3 : pollution de Object.prototype.NODE_OPTIONS = --require=/tmp/evil.js
[*] attente que l'exécuteur bloqué expire, soit redémarré et hérite de NODE_OPTIONS ...
RCE uid=1000(node) gid=1000(node) groups=1000(node),1000(node)
uid=1000(node) est le compte de service de l'exécuteur et la sortie est l'état
id/uname en direct — une exécution réelle, pas un écho d'entrée.
Tout utilisateur authentifié pouvant créer ou modifier un workflow obtient une exécution de commande OS sur l'hôte n8n, contournant le sandbox du nœud Code — compromission totale du serveur d'automatisation et de chaque identifiant/système auquel il peut accéder.
Object.create(null), tuant l'écriture __proto__).--disable-proto=delete /
--disallow-code-generation-from-strings, mais ceux-ci protègent l'exécuteur,
pas le processus principal où la pollution atterrit.Signalez les workflows dont les paramètres de pagination HTTP Request utilisent des
valeurs type de __proto__ / constructor / prototype, et NODE_OPTIONS apparaissant
comme une erreur de propriété d'entité dans les logs n8n/TypeORM.
Voir ANALYSIS.md pour la primitive, le comportement de l'environnement
for…in de Node, le cycle de vie de l'exécuteur et le correctif.