Root cause
Injection & unsafe execution
chore: Bundle 2.x (#28844)
How AI contributed: Flawed AI-written codePublished May 14, 2026 in
n8n-io/n8nAll 6 findings in this repository
XML node prototype-pollution patch bypass: the GHSA-hqr4-h3xv-9m3r patch validated String(options.attrkey ?? '') / charkey against forbidden keys but forwarded the raw (uncoerced) options object to xml2js Parser/Builder; a stateful non-string value (toString() returning a safe string on first call, attacker key on later calls) passes validation yet coerces to __proto__/constructor when the parser uses it, restoring prototype pollution -> RCE.
Root cause
chore: Bundle 2.x (#28844)
Fix
The fix converts attrkey and charkey to strings in the options object itself before checking forbidden XML names, so xml2js receives the same validated values instead of later re-coercing attacker-controlled objects.
AI-assisted fix: Claude Code · n8n-assistant[bot]
Case facts
@@ -3,6 +3,7 @@ import type { AxiosError } from 'axios'; import { parseString } from 'xml2js'; +import { removeCircularRefs, sanitizeXmlName } from '../utils'; import { NodeError } from './abstract/node.error'; import type { ErrorLevel } from '@n8n/errors'; import {@@ -19,7 +20,6 @@ import type { Functionality, RelatedExecution, } from '../interfaces';-import { removeCircularRefs } from '../utils'; export interface NodeOperationErrorOptions { message?: string;@@ -275,16 +275,24 @@ export class NodeApiError extends NodeError { } private setDescriptionFromXml(xml: string) {- parseString(xml, { explicitArray: false }, (_, result) => {- if (!result) return;-- const topLevelKey = Object.keys(result)[0];- this.description = this.findProperty(- result[topLevelKey],- POSSIBLE_ERROR_MESSAGE_KEYS,- POSSIBLE_NESTED_ERROR_OBJECT_KEYS,- );- });+ parseString(+ xml,+ {+ explicitArray: false,+ tagNameProcessors: [sanitizeXmlName],+ attrNameProcessors: [sanitizeXmlName],+ },+ (_, result) => {+ if (!result) return;++ const topLevelKey = Object.keys(result)[0];+ this.description = this.findProperty(+ result[topLevelKey],+ POSSIBLE_ERROR_MESSAGE_KEYS,+ POSSIBLE_NESTED_ERROR_OBJECT_KEYS,+ );+ },+ ); } /**@@ -4,7 +4,7 @@ import type { INodeType, INodeTypeDescription, } from 'n8n-workflow';-import { NodeConnectionTypes, NodeOperationError, deepCopy } from 'n8n-workflow';+import { NodeConnectionTypes, NodeOperationError, deepCopy, sanitizeXmlName } from 'n8n-workflow'; import { Builder, Parser } from 'xml2js'; export class Xml implements INodeType {@@ -236,16 +236,18 @@ export class Xml implements INodeType { const options = this.getNodeParameter('options', 0, {}); const forbiddenKeys = ['__proto__', 'constructor', 'prototype'];- if (typeof options.attrkey === 'string' && forbiddenKeys.includes(options.attrkey)) {+ const attrkey = String(options.attrkey ?? '');+ const charkey = String(options.charkey ?? '');+ if (forbiddenKeys.includes(attrkey)) { throw new NodeOperationError( this.getNode(),- `The "Attribute Key" option value "${options.attrkey}" is not allowed`,+ `The "Attribute Key" option value "${attrkey}" is not allowed`, ); }- if (typeof options.charkey === 'string' && forbiddenKeys.includes(options.charkey)) {+ if (forbiddenKeys.includes(charkey)) { throw new NodeOperationError( this.getNode(),- `The "Character Key" option value "${options.charkey}" is not allowed`,+ `The "Character Key" option value "${charkey}" is not allowed`, ); } @@ -262,6 +264,10 @@ export class Xml implements INodeType { explicitArray: false, }, options,+ {+ tagNameProcessors: [sanitizeXmlName],+ attrNameProcessors: [sanitizeXmlName],+ }, ); const parser = new Parser(parserOptions);@@ -6,7 +6,7 @@ import type { IRequestOptions, IHttpRequestMethods, } from 'n8n-workflow';-import { NodeApiError, NodeOperationError, sleep } from 'n8n-workflow';+import { NodeApiError, NodeOperationError, sanitizeXmlName, sleep } from 'n8n-workflow'; import { parseString } from 'xml2js'; import {@@ -91,9 +91,17 @@ export function formatSearch(responseData: SplunkSearchResponse) { export async function parseXml(xml: string) { return await new Promise((resolve, reject) => {- parseString(xml, { explicitArray: false }, (error, result) => {- error ? reject(error) : resolve(result);- });+ parseString(+ xml,+ {+ explicitArray: false,+ tagNameProcessors: [sanitizeXmlName],+ attrNameProcessors: [sanitizeXmlName],+ },+ (error, result) => {+ error ? reject(error) : resolve(result);+ },+ ); }); } @@ -236,19 +236,23 @@ export class Xml implements INodeType { const options = this.getNodeParameter('options', 0, {}); const forbiddenKeys = ['__proto__', 'constructor', 'prototype'];- const attrkey = String(options.attrkey ?? '');- const charkey = String(options.charkey ?? '');- if (forbiddenKeys.includes(attrkey)) {- throw new NodeOperationError(- this.getNode(),- `The "Attribute Key" option value "${attrkey}" is not allowed`,- );+ if (options.attrkey !== undefined) {+ options.attrkey = String(options.attrkey);+ if (forbiddenKeys.includes(options.attrkey)) {+ throw new NodeOperationError(+ this.getNode(),+ `The "Attribute Key" option value "${options.attrkey}" is not allowed`,+ );+ } }- if (forbiddenKeys.includes(charkey)) {- throw new NodeOperationError(- this.getNode(),- `The "Character Key" option value "${charkey}" is not allowed`,- );+ if (options.charkey !== undefined) {+ options.charkey = String(options.charkey);+ if (forbiddenKeys.includes(options.charkey)) {+ throw new NodeOperationError(+ this.getNode(),+ `The "Character Key" option value "${options.charkey}" is not allowed`,+ );+ } } let item: INodeExecutionData;AI-assisted change a3b8cf3f9817b7a272d5f5ec3791e9e5801604affde84bed2e5f51241b485d20 · Fix 3b22021ea6f164d98e2d97a59e973cc0e7271d3fe3d8be32fdc02e5e3b3ce7bb