fix(Form Node): Preserve CSS child combinator selectors in custom styling (#24106)

Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com>
This commit is contained in:
Dawid Myslak
2026-01-14 10:00:07 +00:00
committed by GitHub
co-authored by Michael Kret
parent c6eb487518
commit 680f264731
2 changed files with 52 additions and 2 deletions
@@ -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; }<script>alert("xss")</script>';
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; }</style><script>alert("xss")</script>';
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<IWebhookFunctions>();
executeFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any);
@@ -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(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/g, '&'),
});
}