import WorkflowValidator from '../resources/WorkflowValidator.js';
/**
* Validateur pour les aspects de sécurité dans les workflows n8n
*/
class SecurityValidator implements WorkflowValidator {
/**
* Valide les aspects de sécurité dans un workflow n8n
* @param workflow Les données du workflow à valider
* @param strictness Le niveau de rigueur de la validation
* @returns Résultat de la validation avec les problèmes détectés
*/
validate(workflow: any, strictness: 'low' | 'medium' | 'high') {
const issues: Array<{
message: string;
recommendation: string;
severity: 'low' | 'medium' | 'high';
location?: string;
}> = [];
// Vérifier si le workflow est partagé publiquement
if (workflow.active && workflow.settings?.callerPolicy === 'any') {
issues.push({
message: 'Le workflow est actif et accessible publiquement',
recommendation: 'Limitez l\'accès au workflow en configurant une politique d\'appelant restrictive',
severity: 'high',
});
}
// Vérifier les informations sensibles en clair dans les nœuds
if (workflow.nodes && Array.isArray(workflow.nodes)) {
workflow.nodes.forEach((node: any) => {
// Vérifier les informations d'identification en clair
this.checkForSensitiveData(node, issues);
// Vérifier les nœuds HTTP pour les problèmes de sécurité
if ((node.type || '').toLowerCase().includes('http')) {
this.checkHttpNodeSecurity(node, issues, strictness);
}
// Vérifier les nœuds d'exécution de code
if (this.isCodeExecutionNode(node)) {
this.checkCodeExecutionSecurity(node, issues, strictness);
}
});
}
// Vérifier les webhooks pour l'authentification
this.checkWebhookSecurity(workflow, issues, strictness);
// Vérifier si le workflow utilise des variables d'environnement pour les secrets
this.checkEnvironmentVariableUsage(workflow, issues, strictness);
return {
valid: issues.length === 0,
issues,
};
}
/**
* Vérifie si un nœud contient des informations sensibles en clair
*/
private checkForSensitiveData(node: any, issues: any[]): void {
// Patterns pour détecter les informations sensibles
const sensitivePatterns = [
{ pattern: /password\s*[:=]\s*['"](?!{{)([^'"{}]*)['"]/, type: 'mot de passe' },
{ pattern: /api[_\s]*key\s*[:=]\s*['"](?!{{)([^'"{}]*)['"]/, type: 'clé API' },
{ pattern: /token\s*[:=]\s*['"](?!{{)([^'"{}]*)['"]/, type: 'token' },
{ pattern: /secret\s*[:=]\s*['"](?!{{)([^'"{}]*)['"]/, type: 'secret' },
{ pattern: /auth\s*[:=]\s*['"](?!{{)([^'"{}]*)['"]/, type: 'authentification' },
];
// Convertir le nœud en chaîne pour la recherche
const nodeString = JSON.stringify(node);
// Vérifier chaque pattern
sensitivePatterns.forEach(({ pattern, type }) => {
if (pattern.test(nodeString)) {
issues.push({
message: `Le nœud "${node.name}" contient un ${type} en clair`,
recommendation: `Utilisez des variables d'environnement ou des credentials n8n pour stocker les ${type}s de manière sécurisée`,
severity: 'high',
location: node.id,
});
}
});
}
/**
* Vérifie la sécurité des nœuds HTTP
*/
private checkHttpNodeSecurity(node: any, issues: any[], strictness: string): void {
// Vérifier si le nœud HTTP utilise HTTPS
if (node.parameters?.url && typeof node.parameters.url === 'string' &&
node.parameters.url.startsWith('http:') && !node.parameters.url.includes('localhost')) {
issues.push({
message: `Le nœud "${node.name}" utilise HTTP non sécurisé`,
recommendation: 'Utilisez HTTPS pour toutes les requêtes externes',
severity: strictness === 'low' ? 'medium' : 'high',
location: node.id,
});
}
// Vérifier si le nœud HTTP vérifie les certificats SSL
if (node.parameters?.allowUnauthorizedCerts === true) {
issues.push({
message: `Le nœud "${node.name}" autorise les certificats SSL non vérifiés`,
recommendation: 'Activez la vérification des certificats SSL pour éviter les attaques de type man-in-the-middle',
severity: 'high',
location: node.id,
});
}
}
/**
* Vérifie si un nœud est de type exécution de code
*/
private isCodeExecutionNode(node: any): boolean {
const type = (node.type || '').toLowerCase();
return type.includes('code') ||
type.includes('script') ||
type.includes('function') ||
type.includes('execute') ||
type.includes('eval');
}
/**
* Vérifie la sécurité des nœuds d'exécution de code
*/
private checkCodeExecutionSecurity(node: any, issues: any[], strictness: string): void {
// Vérifier si le code contient des imports potentiellement dangereux
const dangerousImports = [
'child_process', 'fs', 'path', 'os', 'crypto', 'net', 'dns', 'http', 'https'
];
const code = node.parameters?.code || node.parameters?.jsCode || '';
if (typeof code === 'string') {
dangerousImports.forEach(importName => {
if (code.includes(`require('${importName}')`) || code.includes(`require("${importName}")`)) {
issues.push({
message: `Le nœud "${node.name}" utilise le module système potentiellement dangereux '${importName}'`,
recommendation: 'Évitez d\'utiliser des modules système dans les nœuds de code pour réduire les risques de sécurité',
severity: strictness === 'low' ? 'medium' : 'high',
location: node.id,
});
}
});
// Vérifier l'utilisation d'eval ou de new Function
if (code.includes('eval(') || code.includes('new Function(')) {
issues.push({
message: `Le nœud "${node.name}" utilise eval() ou new Function() qui sont des pratiques dangereuses`,
recommendation: 'Évitez d\'utiliser eval() ou new Function() car ils peuvent exécuter du code arbitraire',
severity: 'high',
location: node.id,
});
}
}
}
/**
* Vérifie la sécurité des webhooks
*/
private checkWebhookSecurity(workflow: any, issues: any[], strictness: string): void {
if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
return;
}
const webhookNodes = workflow.nodes.filter((node: any) =>
(node.type || '').toLowerCase().includes('webhook')
);
webhookNodes.forEach((node: any) => {
// Vérifier si le webhook a une authentification
const hasAuth = node.parameters?.authentication === true ||
(node.parameters?.authentication && node.parameters.authentication !== 'none');
if (!hasAuth) {
issues.push({
message: `Le nœud webhook "${node.name}" n'a pas d'authentification configurée`,
recommendation: 'Configurez l\'authentification pour les webhooks afin de limiter l\'accès',
severity: strictness === 'low' ? 'medium' : 'high',
location: node.id,
});
}
});
}
/**
* Vérifie l'utilisation des variables d'environnement pour les secrets
*/
private checkEnvironmentVariableUsage(workflow: any, issues: any[], strictness: string): void {
if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
return;
}
// Compter le nombre de références aux variables d'environnement
let envVarCount = 0;
const workflowString = JSON.stringify(workflow);
// Rechercher les références aux variables d'environnement (pattern: {{$env.VARIABLE_NAME}})
const envVarMatches = workflowString.match(/\{\{\$env\.[A-Za-z0-9_]+\}\}/g);
envVarCount = envVarMatches ? envVarMatches.length : 0;
// Pour les niveaux de rigueur medium et high, suggérer l'utilisation de variables d'environnement
if (envVarCount === 0 && strictness !== 'low') {
issues.push({
message: 'Le workflow n\'utilise pas de variables d\'environnement pour les secrets',
recommendation: 'Utilisez des variables d\'environnement ({{$env.VARIABLE_NAME}}) pour stocker les informations sensibles',
severity: strictness === 'medium' ? 'low' : 'medium',
});
}
}
}
export default SecurityValidator;