Table of Contents
- BLUF (Bottom Line Up Front)
- Executive Summary
- Key Findings
- Recommendations
- Mitigations
- Detection
- Timeline
- Vulnerability Inventory
- Technical Analysis
- Ni8mare (CVE-2026-21858) - unauthenticated webhook takeover
- Expression Injection (CVE-2025-68613) - authenticated RCE, the chain’s second half
- N8scape (CVE-2025-68668) - Pyodide sandbox escape
- Git Node RCE (CVE-2026-21877) - authenticated file write to code execution
- The Exploit Chain
- MITRE ATT&CK Mapping
- Supply Chain Attack: Sha1-Hulud
- Threat Activity
- References
- Public PoCs
- 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 (CVE-2026-21858) - unauthenticated webhook takeover.
- Expression injection (CVE-2025-68613) - authenticated RCE.
- N8scape (CVE-2025-68668) - Pyodide sandbox escape via Python’s
ctypesFFI. - Git-node file write (CVE-2026-21877) - authenticated file write to RCE.
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.jsprocessobject and executes code. - N8scape (CVE-2025-68668) - the Python Code Node runs on Pyodide, and Python’s own
ctypesmodule 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
- Inventory all n8n instances including development, staging, production, and test environments.
- 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).
- Restrict who can create and edit workflows. The three authenticated RCEs need only a low-privileged workflow account.
- Audit npm dependencies for the Sha1-Hulud worm; remove any trojanized
n8n-nodes-*packages (see Supply Chain Attack: Sha1-Hulud). - Rotate credentials stored in n8n - API keys, OAuth tokens, database credentials, cloud access keys.
- Assume compromise on any internet-exposed, unpatched instance.
- 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
| Vulnerability | Minimum Safe Version | Recommended |
|---|---|---|
| CVE-2026-21858 (Ni8mare) | 1.121.0 | 2.3.0+ |
| CVE-2026-21877 (Git node) | 1.121.3 | 2.3.0+ |
| CVE-2025-68613 (Expression injection) | 1.120.4 / 1.121.1 / 1.122.0 | 2.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.0 | 2.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:
| |
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:
| |
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:
| |
Timeline
| Date | Event | Source |
|---|---|---|
| 2025-11-09 | Dor Attias (Cyera) reports Ni8mare to n8n | Cyera |
| 2025-11-18 | n8n releases 1.121.0, patching CVE-2026-21858 | GitHub Release |
| 2025-11-24 | Sha1-Hulud 2.0 npm worm campaign begins, compromising n8n packages | Microsoft |
| 2025-12-19 | CVE-2025-68613 disclosed (expression injection RCE) | CIRCL |
| 2025-12-22 | Censys reports 103,476 exposed n8n instances | Censys |
| 2025-12-26 | Three critical CVEs disclosed (CVE-2025-68668, CVE-2025-68697, CVE-2025-61914) | GitHub Advisories |
| 2026-01-05 | n8n 2.3.0 released, removing Pyodide entirely and closing CVE-2025-68668 | GitHub Release |
| 2026-01-06 | CVE-2026-21858 assigned; Canadian Cyber Centre advisory AV26-004 published | CCCS |
| 2026-01-08 | Public unauthenticated RCE chain PoC published; scanning activity rises | PoC |
Vulnerability Inventory
Vulnerability intelligence via the Baysec Platform identified 16 n8n CVEs affecting current deployments.
| CVE ID | CVSS | Severity | Published | Patch | Impact |
|---|---|---|---|---|---|
| CVE-2026-21858 | 10.0 | CRITICAL | 2026-01-07 | 1.121.0 | Unauthenticated file read to admin bypass to RCE |
| CVE-2026-21877 | 9.9 | CRITICAL | 2026-01-07 | 1.121.3 | Authenticated arbitrary file write to RCE |
| CVE-2025-68668 | 9.9 | CRITICAL | 2025-12-26 | 2.3.0 | N8scape - Pyodide sandbox escape in Python Code Node |
| CVE-2025-68613 | 9.9 | CRITICAL | 2025-12-19 | 1.120.4+ | Expression injection RCE |
| CVE-2025-55526 | 9.1 | CRITICAL | 2025-08-26 | - | Directory traversal in download_workflow |
| CVE-2025-65964 | 8.8 | HIGH | 2025-12-08 | 1.119.2 | RCE via Git pre-commit hooks |
| CVE-2025-62726 | 8.8 | HIGH | 2025-10-30 | 1.113.0 | Git Node RCE via malicious pre-commit hooks |
| CVE-2025-56265 | 8.8 | HIGH | 2025-09-08 | - | Arbitrary file upload in Chat Trigger |
| CVE-2025-52478 | 8.7 | HIGH | 2025-08-19 | 1.98.2 | Stored XSS in Form Trigger to account takeover |
| CVE-2025-61914 | 7.3 | HIGH | 2025-12-26 | 1.114.0 | Stored XSS in “Respond to Webhook” node |
| CVE-2025-68697 | 7.1 | HIGH | 2025-12-26 | 2.0.0 | Legacy Code node arbitrary file read/write |
| CVE-2025-57749 | 6.5 | MEDIUM | 2025-08-20 | 1.106.0 | Symlink traversal in Read/Write File node |
| CVE-2025-58177 | 5.4 | MEDIUM | 2025-09-15 | 1.107.0 | Stored XSS in LangChain Chat Trigger |
| CVE-2025-46343 | 5.0 | MEDIUM | 2025-04-29 | 1.90.0 | Stored XSS via attachments view |
| CVE-2025-49595 | 4.9 | MEDIUM | 2025-07-03 | 1.99.0 | DoS via malformed binary-data URIs |
| CVE-2025-49592 | 4.6 | MEDIUM | 2025-06-26 | 1.98.0 | Open 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)
| Detail | Value |
|---|---|
| Type | Unauthenticated RCE (no credentials) |
| CVSS 3.1 | 10.0 (Critical) - AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N |
| CWE | CWE-20: Improper Input Validation |
| Authentication | None |
| Fixed in | 1.121.0 (vulnerable 1.65.0 - 1.120.x) |
| Advisory | GHSA-v4pr-fm98-w9pg |
| CISA KEV | Not listed (as of 08.01.2026) |
| Discovery | Dor 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):
| |
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:
- Read
/proc/self/environ-> extractHOME. - Read
{HOME}/.n8n/config-> this JSON file containsencryptionKey, the instance-wide secret n8n uses to sign and encrypt sensitive data. - Read
{HOME}/.n8n/database.sqlite-> the full user table, including theglobal:owner(admin) row’sid,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:
| |
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:
| |
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)
| Detail | Value |
|---|---|
| Type | Authenticated RCE |
| CVSS 3.1 | 9.9 (Critical) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| EPSS | 8.42% (92nd percentile) |
| CWE | CWE-94: Code Injection |
| Authentication | Required (low privilege) |
| Fixed in | 1.120.4 / 1.121.1 / 1.122.0 (vulnerable 0.211.0 onward; 1.121.0 was a regression) |
| Advisory | GHSA-v98v-ff95-f3cp |
| Exploitation | Public PoC (chained with Ni8mare) |
| Discovery | Reported 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:
| |
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:
| |
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:
| |
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:
| |
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)
| Detail | Value |
|---|---|
| Type | Authenticated RCE (Pyodide sandbox escape) |
| CVSS 3.1 | 9.9 (Critical) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L |
| CWE | CWE-693: Protection Mechanism Failure |
| Authentication | Required (low privilege) |
| Fixed in | Native 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) |
| Advisory | GHSA-62r4-hw23-cc8v |
| Discovery | Vladimir 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:
| |
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:
| |
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.
| Date | Change |
|---|---|
| April 2025 | Pyodide sandboxed for the first time |
| June 2025 | Selected module imports blocked |
| July 2025 | os.system blocked |
| July 2025 | Object.constructor.constructor and the js module blocked (code above) |
| December 2025 | Pyodide 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:
| |
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:
| |
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=trueandN8N_NATIVE_PYTHON_RUNNER=trueto 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)
| Detail | Value |
|---|---|
| Type | Authenticated file write to RCE |
| CVSS 3.1 | 9.9 (Critical) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-434: Unrestricted Upload, CWE-94: Code Injection |
| Authentication | Required (low privilege) |
| Fixed in | 1.121.3 (vulnerable 0.123.0 - 1.121.2) |
| Advisory | GHSA-v364-rw7m-3263 |
| Discovery | Theo 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:
| |
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:
| |
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:
| |
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:
POST /form/<form-path>with the LFI body targeting/proc/self/environto recoverHOME, then{HOME}/.n8n/configto recoverencryptionKey, then{HOME}/.n8n/database.sqliteto recover the admin row (bodies shown in Ni8mare).- Locally, forge the
n8n-authcookie:jwt.encode({"id": uid, "hash": h}, secret, algorithm="HS256"), using the secret and hash derived from step 1 (algorithm in Ni8mare). POST /rest/workflowswith that cookie, creating amanualTrigger->Setnode workflow whose string value is the expression-injection payload (shown in Expression Injection).POST /rest/workflows/{id}/run, thenGET /rest/executions/{id}to read the command output.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
| Technique | ID | Description |
|---|---|---|
| Exploit Public-Facing Application | T1190 | Ni8mare exploits exposed webhook and form endpoints |
| Unsecured Credentials | T1552 | Credentials and signing secret read from the n8n database and config |
| Valid Accounts | T1078 | Forged admin session from the recovered signing secret |
| Command and Scripting Interpreter | T1059 | RCE via expression injection, the Git-node write, or N8scape |
| Supply Chain Compromise | T1195 | Sha1-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:
| Package | Malicious Version | Detection Date |
|---|---|---|
| n8n-nodes-phoai-ultimate-tools | 32 versions (1.3.0 - 1.8.1) | 2025-11-26 |
| n8n-nodes-vercel-ai-sdk | 0.1.7 | 2025-11-26 |
| @hapheus/n8n-nodes-pgp | 1.5.1 | 2025-11-26 |
| n8n-nodes-tmdb | 0.5.1 | 2025-11-26 |
| n8n-nodes-viral-app | 0.2.5 | 2025-11-26 |
| n8n-nodes-performance-metrics | all | 2026-01-07 |
| n8n-performance-metrics | all | 2026-01-07 |
Worm Behavior
- A malicious
preinstallscript runs before any security scan. - Drops
setup_bun.jsto install the Bun runtime if absent. - An obfuscated
bun_environment.jsscans for npm tokens, GitHub PATs, AWS/GCP/Azure credentials, and SSH keys. - Publishes stolen secrets to public repos described “Sha1-Hulud: The Second Coming”.
- Creates a self-hosted GitHub runner named
SHA1HULUDand injects.github/workflows/discussion.yamlwith command injection. - Publishes malicious versions of packages owned by the victim.
Indicators and Hunt Queries
| |
- 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)
| Technique | ID | Description |
|---|---|---|
| Supply Chain Compromise | T1195 | Trojanized npm packages |
| Command and Scripting Interpreter: JavaScript | T1059.007 | Pre-install script execution |
| Credentials from Password Stores | T1555 | Harvests npm tokens, GitHub PATs, cloud keys |
| Ingress Tool Transfer | T1105 | Downloads the Bun runtime and payloads |
| Valid Accounts: Cloud Accounts | T1078.004 | Uses stolen credentials for persistence |
| Exfiltration to Code Repository | T1567.001 | Publishes 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
| Source | Count | Date | Scope |
|---|---|---|---|
| Censys | 103,476 | 2025-12-22 | Potentially vulnerable to CVE-2025-68613 |
| Shodan | 71,118 | 2026-01-08 | Instances exposing the n8n banner |
| Cyera | ~100,000 | 2026-01 | Global estimate for Ni8mare |
| Censys (post-disclosure) | 26,512 | 2026-01-08 | Reduced 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
- GHSA-v4pr-fm98-w9pg - CVE-2026-21858 (Ni8mare)
- GHSA-v364-rw7m-3263 - CVE-2026-21877 (Git node)
- GHSA-v98v-ff95-f3cp - CVE-2025-68613 (Expression injection)
- GHSA-62r4-hw23-cc8v - CVE-2025-68668 (N8scape)
- CIRCL - CVE-2026-21858 / CVE-2026-21877 / CVE-2025-68613 / CVE-2025-68668
- Canadian Cyber Centre - AV26-004
- n8n commit - CVE-2026-21877 fix (f4b009d)
Discovery Research
- Cyera Research Labs - Ni8mare (CVE-2026-21858)
- SmartKeyss - Breaking out of the Python sandbox in n8n (CVE-2025-68668)
- gelusus/wxvl - First public PoC for the ctypes bypass (CVE-2025-68668) (09.01.2026)
Active Exploitation and Supply Chain
- Akamai - ZeroBot malware targets n8n (CVE-2025-68613)
- Microsoft - Shai-Hulud 2.0: detection, investigation, defence
- GitLab Advisory - n8n-nodes-vercel-ai-sdk
- GitLab Advisory - n8n-nodes-tmdb
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.
| Repository | CVE | Description | Language |
|---|---|---|---|
| Chocapikk/CVE-2026-21858 | CVE-2026-21858 | Ni8mare unauthenticated file read to RCE chain | Python |
| Ashwesker/Ashwesker-CVE-2026-21858 | CVE-2026-21858 | Alternative Ni8mare exploit | - |
| eduardorossi84/CVE-2026-21858-POC | CVE-2026-21858 | Ni8mare PoC | - |
| AbdulRKB/n8n-RCE | CVE-2026-21858 | n8n RCE exploit | Python |
| wioui/n8n-CVE-2025-68613-exploit | CVE-2025-68613 | RCE exploit and scanner | Python |
| rxerium/CVE-2025-68613 | CVE-2025-68613 | Detection templates and exposure analysis | - |
| Ashwesker/Ashwesker-CVE-2025-68613 | CVE-2025-68613 | Expression injection exploit | - |
| TheStingR/CVE-2025-68613-POC | CVE-2025-68613 | Expression injection PoC and scanner | Python |
| LingerANR/n8n-CVE-2025-68613 | CVE-2025-68613 | Lab to reproduce the vulnerability | Python |
| hackersatyamrastogi/n8n-exploit-CVE-2025-68613-n8n-God-Mode-Ultimate | CVE-2025-68613 | RCE exploit | Python |
| mbanyamer/n8n-Authenticated-Expression-Injection-RCE-CVE-2025-68613 | CVE-2025-68613 | Authenticated RCE PoC | Shell |
| manyaigdtuw/CVE-2025-68613_Scanner | CVE-2025-68613 | GUI Shodan-powered scanner | Python |
| Shisui6/CVE-2025-68613 | CVE-2025-68613 | Expression injection PoC | Python |
| DarkTigerET/CVE-2025-68613 | CVE-2025-68613 | Alternative exploit implementation | - |
| imjdl/CVE-2025-68613-EXP | CVE-2025-68613 | RCE exploit tool | Python |
| Trickest CVE-2025-55526 | CVE-2025-55526 | Directory traversal exploit metadata | - |
| gelusus/wxvl - CVE-2025-68668 | CVE-2025-68668 | ctypes FFI sandbox bypass, first public write-up | Python |
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.