XSS Vectors in n8n Custom Code Nodes Explained: Complete …

Step by Step Guide to solve xss vectors in custom code 
Step by Step Guide to solve xss vectors in custom code


Who this is for: n8n developers who write custom JavaScript nodes and need to protect workflows that expose data to browsers (webhooks, UI pages, email templates, etc.). We cover this in detail in the n8n Security & Hardening Guide.


Quick diagnosis

Custom JavaScript nodes that embed user‑supplied data into HTML, JSON, or HTTP responses are prime XSS injection points. The fastest remediation is to sanitize every external value before it reaches a browser context (e.g., innerHTML, res.send(), return). Use a proven library such as DOMPurify or n8n’s built‑in sanitize helper, then re‑test the node with the n8n test runner.


1. Understanding XSS in n8n custom code

If you encounter any database injection risks resolve them before continuing with the setup.

Browser contexts & typical entry points

Browser context Typical n8n entry point
HTML return value rendered in a UI webview or email template
JavaScript eval‑style string concatenation in a downstream node
URL Query string built for an HTTP request
JSONP Callback wrapper returned to a client

Vulnerable patterns

Example vulnerable pattern
return `

${item.json.userInput}

`;

this.helpers.executeWorkflow(`alert(‘${item.json.payload}’)`);
requestOptions.url = `https://example.com/search?q=${item.json.term}`;
return `${item.json.callback}(${JSON.stringify(data)})`;

EEFA note: In production, n8n runs under the n8n user inside a Docker container. Even with container isolation, XSS can still compromise any client that consumes the workflow’s output (e.g., a public dashboard). Treat XSS as a client‑side breach with server‑side impact.


2. Common XSS injection vectors

Vector Source of data
User‑provided query parameters this.getNodeParameter('query') from a webhook
Headers from upstream APIs item.json.headers['User-Agent']
File contents uploaded via HTTP node item.binary.data decoded to string
Environment variables process.env.CUSTOM_MESSAGE
Dynamic workflow names this.getWorkflowStaticData('node')

How it reaches the browser – each source can be interpolated into HTML, JavaScript, URLs, or JSONP that later renders in a client’s browser.

EEFA note: Environment variables are often considered “safe” because they’re set by the operator, but if they are populated from external sources (e.g., CI pipelines), they become attack vectors. Validate them the same way as user input. If you encounter any privilege escalation workflow execution resolve them before continuing with the setup.


3. Step‑by‑step audit: find XSS in a custom JavaScript node

Micro‑summary: Identify every external value, map it to a browser context, and replace direct interpolation with a sanitizer.

  1. Locate external data accesses – search for this.getNodeParameter, item.json, item.binary, process.env, and any awaited HTTP responses.
  2. Map each value to a browser context – does it end up in HTML, JavaScript, URL, or JSONP?
  3. Flag direct interpolation – look for template literals or string concatenation that inject the value without escaping.

Vulnerable snippet (before)

// Direct interpolation – unsafe
return;

Fixed snippet (after) – using n8n’s built‑in helper

// Escape HTML before insertion
const safe = this.helpers.escapeHTML(item.json.userInput);
return;

4. Add a unit test that sends a malicious payload and asserts the output is clean.

Test skeleton

import { runWorkflow } from 'n8n-core';

test('XSS mitigation', async () => {
  const result = await runWorkflow('my-xss-node', {
    userInput: '',
  });
  expect(result).not.toContain('onerror');
});

EEFA note: The test runner executes workflows in an isolated Docker container, but the test code itself runs on the host. Ensure the host environment does not expose internal state via NODE_OPTIONS=--inspect in CI.


4. Sanitization strategies

Micro‑summary: Choose the lightest sanitizer that satisfies the data’s complexity; prefer built‑in helpers for simple text and DOMPurify for rich HTML.

4.1 Built‑in HTML escape

// Simple text escaping
const safe = this.helpers.escapeHTML(value);

4.2 DOMPurify (server‑side)

// Install once: npm install dompurify jsdom
const { JSDOM } = require('jsdom');
const createDOMPurify = require('dompurify');
// Create a reusable purifier
function getPurifier() {
  const window = new JSDOM('').window;
  return createDOMPurify(window);
}
// Sanitize rich HTML fragments
function sanitizeHTML(input) {
  const DOMPurify = getPurifier();
  return DOMPurify.sanitize(input, {
    ALLOWED_TAGS: ['b', 'i', 'strong', 'em', 'a'],
    ALLOWED_ATTR: ['href', 'title'],
  });
}
// Example usage in a node
const raw = item.json.richText; // user supplied
const safeHtml = sanitizeHTML(raw);
return safeHtml;

4.3 validator.js for URL‑safe values

const { escape } = require('validator');
const safe = escape(userInput);

4.4 Custom whitelist (high‑performance)

const allowed = /^[a-zA-Z0-9 _-]+$/;
if (!allowed.test(value)) {
  throw new Error('Invalid characters');
}

4.5 Content‑Security‑Policy (defense‑in‑depth)

res.setHeader(
  'Content-Security-Policy',
  "default-src 'self'; script-src 'self'"
);

Recommended default for n8n custom nodes – use the DOMPurify helper for any HTML fragment; fall back to escapeHTML for plain text.

EEFA note: When using jsdom inside n8n, ensure the container has at least 256 MiB of RAM; DOMPurify can be memory‑intensive for large payloads. Cache the window instance outside the node’s execute method for high‑throughput workflows.


5. Production‑grade XSS hardening checklist

Micro‑summary: Verify sanitization, CSP, dependency hygiene, static analysis, monitoring, and container hardening.

Item Description
All external values are sanitized Every this.getNodeParameter, item.json, item.binary, process.env value passes through a sanitizer before reaching a browser context.
CSP header set on webhook responses Prevents inline script execution even if a payload slips through.
Node dependencies are locked package-lock.json pinned; no vulnerable versions of DOMPurify or validator.
Static analysis enabled Custom ESLint rule no-unsanitized flags direct interpolation.
Runtime monitoring Log any sanitization failures with request ID for forensic review.
Least‑privilege container Node runs as non‑root user; filesystem write access limited to /data.

Verification steps – run the unit test suite with known XSS payloads, inspect response headers with curl -I, run npm audit, ensure CI fails on ESLint warnings, and review logs for SanitizationError entries.


6. Automated testing & continuous integration

Micro‑summary: Add dedicated XSS tests, inject OWASP cheat‑sheet payloads, and enforce coverage thresholds.

  1. Create a test file (tests/xss.test.js).
  2. Inject payloads from the OWASP XSS Filter Evasion Cheat Sheet.
  3. Assert the output is clean and that a sanitization log entry appears.

Payload array

const payloads = [
  '<svg/onload=alert(1)>'
];

Test suite

import { executeWorkflow } from 'n8n-core';

describe('XSS protection in custom JS node', () => {
  payloads.forEach(p => {
    test(`sanitizes ${p}`, async () => {
      const result = await executeWorkflow('my-custom-node', {
        userInput: p,
      });
      expect(result).not.toMatch(/<script|onerror|onload/);
    });
  });
});

CI tip: Fail the pipeline if coverage for sanitizeHTML drops below 95 % or if any ESLint no-unsanitized warnings appear.


Final Fix

const safe = this.helpers.escapeHTML(item.json.userInput); // or DOMPurify for rich HTML
return `
${safe}

`;

Replace every direct interpolation in custom JavaScript nodes with a sanitizer, run the XSS unit tests, and enforce the checklist in CI to eliminate XSS vectors from your n8n workflows.

Leave a Comment

Your email address will not be published. Required fields are marked *