Feb 5, 2026·7 min read·116 visits
n8n interprets strings in double curly braces as code. By crafting a malicious `.git/config` file (e.g., in a repo cloned by n8n), an attacker can inject JavaScript payloads. When the Git node lists the config, n8n evaluates the payload, leading to full RCE.
A critical Remote Code Execution (RCE) vulnerability in n8n's Git node allows authenticated users to execute arbitrary commands via malicious Git configuration values. This creates a classic sandbox escape scenario where data is treated as code.
We love low-code platforms. They empower marketing teams to build complex automations without bothering the engineering department. But there's a dark side to this democratization of logic: the abstraction layer. Under the hood, platforms like n8n are essentially massive, always-on evaluation engines that take input from one place, treat it like a magical template, and shove it into another place.
In this episode of "Why We Can't Have Nice Things," we are looking at CVE-2026-25049, a critical RCE in n8n. The vulnerability lies in the Git Node. This component is designed to help you manage version control within your workflows—pulling repos, pushing changes, and listing configurations. It sounds innocent enough.
However, n8n has a feature—or a bug, depending on who you ask—called "Expression Evaluation." Anything wrapped in {{ }} is treated as JavaScript and executed. The problem arises when the platform trusts external data sources to not contain these magic delimiters. Spoiler alert: Git config files are just text files, and they definitely shouldn't be trusted.
To understand this exploit, you have to understand how n8n processes data. When a node runs, it outputs JSON. When the next node runs, it can reference that JSON. If you drag-and-drop a value in the n8n UI, it creates an expression like {{ $node["Git"].json["remote.origin.url"] }}.
The vulnerability is a classic case of Improper Control of Dynamically-Managed Code Resources (CWE-913), specifically Expression Injection. The Git node's "List Config" operation reads the .git/config file, parses the INI format, and spits out a JSON object containing keys like remote.origin.url.
Here is the logic flaw: The Git node didn't sanitize the values it read from disk. It just passed them along as strings. If an attacker controls the git repository (or can inject into the config), they can set the URL to something like https://{{require('child_process').execSync('calc')}}@github.com.
When the Git node runs, it successfully reads that string. It's just text. But the moment a subsequent node references that value, the n8n expression engine wakes up. It sees the curly braces, assumes it's a template meant for it, and executes the JavaScript code inside. It's a delayed-trigger landmine.
Let's look at the implementation. The vulnerability existed in packages/nodes-base/nodes/Git/GenericFunctions.ts. The code was blindly trusting the output of the git command.
Before the patch, the code essentially did this (simplified):
// Pseudocode of the vulnerability
const config = await git.listConfig();
return config.all; // Returns raw strings containing {{...}}In commit 78608969 (and follow-up 936c06cf), the developers realized that allowing raw strings from a config file to pass through to the expression engine was a bad idea. They introduced a sanitizeUrl function.
> [!NOTE]
> The fix relies on the JavaScript URL constructor. By parsing the string as a URL object and then stringifying it back, special characters like { and } are percent-encoded (e.g., %7B).
Here is the essence of the patch:
// The mitigation strategy
const sanitizeUrl = (url: string) => {
try {
const urlObj = new URL(url);
urlObj.password = ''; // Also strips creds, nice bonus
urlObj.username = '';
return urlObj.toString(); // Returns encoded string
} catch (error) {
return url;
}
};While this stops the remote.origin.url vector, it feels like a game of Whac-A-Mole. If the attacker finds a config value that isn't run through this sanitizer but is displayed by the node, the game is back on.
Exploiting this requires a bit of setup, but it's devastatingly reliable. We need to trick n8n into processing a Git config file we control. This could happen if the workflow clones a public repository we own, or if we have partial access to the system.
.git/config file to include a Node.js payload in a standard field.require('child_process') primitive, which is available in the n8n execution environment.[remote "origin"]
url = https://{{require('child_process').execSync('cat /etc/passwd').toString()}}@github.com/evil/repo.gitList Config. Connect it to a Debug Node (or any node that references the output).{{ $json["remote.origin.url"] }}, the engine parses the inner injection.The result isn't just a string; it's the output of the command. You have effectively turned a configuration reader into a webshell.
As a researcher, looking at the patch (Commit 936c06cf), I see a potential logical gap. The patch explicitly applies sanitizeUrl to remote.origin.url and remote.origin.pushurl.
// Selective sanitization?
if (config.values['remote.origin.url']) {
config.values['remote.origin.url'] = sanitizeUrl(config.values['remote.origin.url']);
}However, Git config files are flexible. What about remote.upstream.url? What about submodule.name.url? Or even standard fields like user.email? If the Git node outputs all keys found in the config file using a spread operator or a loop, and only sanitizes the "known bad" ones, the vulnerability persists.
If I were attacking a "patched" version, I would immediately fuzz other Git configuration keys. If n8n displays user.name and I set my git username to {{require('fs').writeFileSync(...)}}, I might still achieve RCE. This selective patching strategy is often fragile against creative attackers.
This is a CVSS 9.4 for a reason. n8n is often the "brain" of an organization's operations. It holds API keys for Stripe, Slack, AWS, CRMs, and databases.
If an attacker gains RCE on the n8n host:
This isn't just "I hacked a server." It's "I hacked your business logic."
The immediate fix is obvious: Update. n8n versions 1.123.17 and 2.5.2 include the patch. But let's look at defense-in-depth, because relying on a regex-based URL sanitizer is risky business.
/var/run/docker.sock) unless absolutely necessary. If you do, an RCE in n8n is root on the host.For the developers out there: Stop using eval (or its moral equivalents) on data you didn't create. If you must have a template engine, ensure the context allows strictly for data substitution, not arbitrary code execution.
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H| Product | Affected Versions | Fixed Version |
|---|---|---|
n8n n8n-io | < 1.123.17 | 1.123.17 |
n8n n8n-io | < 2.5.2 | 2.5.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-913 |
| Attack Vector | Network |
| CVSS Score | 9.4 (Critical) |
| Impact | Remote Code Execution (RCE) |
| Vulnerable Component | Git Node (List Config operation) |
| Exploit Complexity | Low |
Improper Control of Dynamically-Managed Code Resources
netfoil, an allowlist-based DNS proxy, failed to sanitize ALPN fields parsed from untrusted DNS-over-HTTPS (DoH) HTTPS Resource Records. This allowed attackers to inject ANSI escape sequences into log files or trigger Denial of Service (DoS) via uncontrolled memory allocations.
An issue was discovered in the tokio-postgres library for Rust prior to version 0.7.18. A trust assumption mismatch between the PostgreSQL protocol messages sent by a server and how they are parsed and indexed by the client-side library allows a rogue or compromised database server to trigger a Denial of Service (DoS) crash via an unhandled out-of-bounds slice indexing panic.
CVE-2026-14669 is a critical heap-based buffer overflow vulnerability in PostgreSQL's date/time formatting function to_char(timestamptz). The flaw arises from unsafe copying of user-controlled timezone abbreviations into a fixed-size internal buffer. An authenticated database user can trigger this issue by setting a long POSIX timezone abbreviation containing custom formatting, allowing them to overwrite adjacent heap structures and hijack execution control to achieve remote code execution (RCE) with the privileges of the 'postgres' operating system user.
An unauthenticated remote denial of service vulnerability exists in the Unleash feature management platform. By submitting a crafted JSON payload containing deeply nested structures to an OpenAPI-validated endpoint, an attacker can trigger uncontrolled recursion within the error formatting module. This leads to a call-stack exhaustion (RangeError: Maximum call stack size exceeded) inside the Node.js runtime, causing the service to crash immediately without recovery.
CVE-2026-63004 is a server-side request forgery (SSRF) vulnerability in the Unleash feature management platform. Authenticated administrators with CREATE_ADDON or UPDATE_ADDON privileges can exploit this vulnerability to initiate requests to loopback addresses, private networks, and cloud metadata endpoints, potentially leading to information disclosure and credential extraction.
Prior to version 8.0.3, Unleash's Markdown event formatter directly mutated the global template-escaping function of the shared mustache Node.js module, resulting in a process-wide security degradation where HTML/Markdown escaping was permanently disabled for the application lifetime.