Feb 5, 2026·7 min read·59 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