diff --git a/packages/nodes-base/nodes/Form/test/utils.test.ts b/packages/nodes-base/nodes/Form/test/utils.test.ts index 9cdd10cb006..afa184703b6 100644 --- a/packages/nodes-base/nodes/Form/test/utils.test.ts +++ b/packages/nodes-base/nodes/Form/test/utils.test.ts @@ -21,6 +21,7 @@ import { resolveRawData, isFormConnected, sanitizeHtml, + sanitizeCustomCss, validateResponseModeConfiguration, prepareFormFields, addFormResponseDataToReturnItem, @@ -347,6 +348,55 @@ describe('FormTrigger, sanitizeHtml', () => { }); }); +describe('sanitizeCustomCss', () => { + it('should return undefined for undefined input', () => { + expect(sanitizeCustomCss(undefined)).toBeUndefined(); + }); + + it('should return undefined for empty string', () => { + expect(sanitizeCustomCss('')).toBeUndefined(); + }); + + it('should preserve CSS child combinator selectors (>)', () => { + const css = '#n8n-form > div.form-header > p { text-align: left; }'; + expect(sanitizeCustomCss(css)).toBe(css); + }); + + it('should preserve CSS adjacent sibling selectors (+)', () => { + const css = 'h1 + p { margin-top: 0; }'; + expect(sanitizeCustomCss(css)).toBe(css); + }); + + it('should preserve CSS general sibling selectors (~)', () => { + const css = 'h1 ~ p { color: gray; }'; + expect(sanitizeCustomCss(css)).toBe(css); + }); + + it('should remove script tags from CSS', () => { + const css = '.container { color: red; }'; + expect(sanitizeCustomCss(css)).toBe('.container { color: red; }'); + }); + + it('should remove style closing tags that could break out of style block', () => { + const css = '.container { color: red; }'; + expect(sanitizeCustomCss(css)).toBe('.container { color: red; }'); + }); + + it('should preserve url() in CSS', () => { + const css = '.bg { background-image: url(https://example.com/image.png); }'; + expect(sanitizeCustomCss(css)).toBe(css); + }); + + it('should preserve complex CSS with multiple selectors and properties', () => { + const css = ` + #n8n-form > div.form-header > p { text-align: left; } + .form-container > .input-group + .input-group { margin-top: 1rem; } + button:hover { background-color: #0056b3; } + `; + expect(sanitizeCustomCss(css)).toBe(css); + }); +}); + describe('FormTrigger, formWebhook', () => { const executeFunctions = mock(); executeFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any); diff --git a/packages/nodes-base/nodes/Form/utils/utils.ts b/packages/nodes-base/nodes/Form/utils/utils.ts index 5e320c45f9f..2651c6f92c2 100644 --- a/packages/nodes-base/nodes/Form/utils/utils.ts +++ b/packages/nodes-base/nodes/Form/utils/utils.ts @@ -131,8 +131,8 @@ export function sanitizeCustomCss(css: string | undefined): string | undefined { return sanitize(css, { allowedTags: [], // No HTML tags allowed allowedAttributes: {}, // No attributes allowed - // This ensures we're only keeping the text content - // which should be the CSS, while removing any HTML/script tags + // Decode HTML entities that sanitize-html encodes, as they break CSS selectors like ">" + textFilter: (text) => text.replace(/>/g, '>').replace(/</g, '<').replace(/&/g, '&'), }); }