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, '&'),
});
}