From a Schema Name to RCE in n8n
n8n used a user-supplied schema name as a key on a plain object. Setting it to __proto__ polluted the prototype and led to RCE through the Git node.
How I Found It
While auditing n8n’s node implementations, I started looking for places where user-supplied strings end up as property keys on plain objects.
I grepped through the nodes-base package for missing checks on __proto__, constructor, and prototype, and the GSuiteAdmin node stood out.
The node has a “Custom Fields” section for user create and update operations. It lets you specify a schema name, field name, and value. All three come from the workflow configuration, which means an attacker with editor access controls them. The schema name is used directly as a dynamic key to group fields:
customSchemas[schemaName] ??= {};
(customSchemas[schemaName] as IDataObject)[fieldName] = value;
That’s the whole bug. If schemaName is "__proto__", you’re writing to Object.prototype.
Technical Details
The Vulnerable Code
The GSuiteAdmin node handles custom schema fields in both the user create (line 520-521) and update (line 802-803) operations with identical code:
const customSchemas: IDataObject = {};
customFields.forEach((field) => {
const { schemaName, fieldName, value } = field as {
schemaName: string;
fieldName: string;
value: string;
};
customSchemas[schemaName] ??= {}; // (1)
(customSchemas[schemaName] as IDataObject)[fieldName] = value; // (2)
});
When schemaName is "__proto__":
customSchemas["__proto__"]triggers the__proto__getter, which returnsObject.prototype. It’s not nullish, so the??=assignment is a no-op(Object.prototype)[fieldName] = valuewrites an attacker-controlled string directly onto the global object prototype
Every plain object created after this point inherits the polluted property.
From Pollution to Code Execution
The pollution alone caused a TypeORM crash, which I cover below. It also chained into RCE through the same gadget I found in the XML node report.
The chain works like this:
-
simple-git creates a plain env object. When the Git node calls
.env(), simple-git allocates{}to hold environment variables. This object inherits fromObject.prototype. -
Node.js
spawn()inherits polluted properties. When building the child process environment, Node.js iterates the env object’s properties, including inherited ones from the polluted prototype. -
Git respects
GIT_SSH_COMMAND. When git encounters an SSH-style URL, it spawnsGIT_SSH_COMMANDas a shell command. If we polluteObject.prototype.GIT_SSH_COMMAND, it propagates into the git child process and gets executed.
So the full attack is: Webhook → GSuiteAdmin (pollution) → Git (RCE).
Proof of Concept
The workflow setup:
- Webhook node:
POST /rce - GSuiteAdmin node: Resource: User, Operation: Create. Set the Custom Fields schema name, field name, and value to expressions reading from the webhook body.
- Git node: Operation: Clone, pointed at an SSH URL.
A single HTTP request fires the entire chain:
curl -X POST "https://TARGET/webhook/rce" \
-H "Content-Type: application/json" \
-d '{
"schemaName": "__proto__",
"fieldName": "GIT_SSH_COMMAND",
"value": "sh -c '\''id; cat /etc/passwd'\'' --"
}'
The GSuiteAdmin node fails at the Google API call, but the pollution has already happened by then. The Git node then spawns git clone with the polluted GIT_SSH_COMMAND, executing the attacker’s command as the n8n process user.
The DoS Side Effect
Even without the RCE chain, the pollution is destructive on its own. After Object.prototype is polluted, TypeORM’s buildWhere function picks up the extra properties via for...in iteration and throws EntityPropertyNotFoundError on every database query. The n8n UI goes unresponsive, all workflow executions fail, and the instance requires a full restart to recover.
Impact
- Remote code execution as the n8n process user on self-hosted, worker mode, and Cloud deployments.
- Full credential theft: the n8n process holds the encryption key for stored credentials.
- Complete denial of service: the TypeORM crash loop makes the instance non-functional until restart.
Remediation
The fix is straightforward: reject dangerous property names before using them as object keys. A blocklist check for __proto__, constructor, and prototype on the schemaName value (or using Object.create(null) for customSchemas) would prevent the pollution entirely.
n8n’s codebase already has a deepMerge utility with prototype pollution guards. The GSuiteAdmin node wasn’t using it.
Timeline
-
Report submitted to n8n security team
-
Advisory and CVE published