From 2a726b432bd449f16aa6a3794683f459a0113aaa Mon Sep 17 00:00:00 2001 From: Dhruwang Date: Tue, 17 Mar 2026 15:42:49 +0530 Subject: [PATCH] fix: pre-strip style attributes before DOMPurify to prevent CSP violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOMPurify internally uses innerHTML to parse HTML strings. When survey content contains style attributes, the browser triggers CSP style-src violations at parse time — before FORBID_ATTR can strip them. Fix: use a simple regex with negated character classes bounded by fixed quote delimiters (O(n), no backtracking) to strip style attributes from the raw string before passing to DOMPurify. Co-Authored-By: Claude Opus 4.6 --- packages/survey-ui/src/lib/utils.ts | 19 +++++++------------ packages/surveys/src/lib/html-utils.ts | 19 +++++++------------ 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/survey-ui/src/lib/utils.ts b/packages/survey-ui/src/lib/utils.ts index 65abe04324..19c035e08c 100644 --- a/packages/survey-ui/src/lib/utils.ts +++ b/packages/survey-ui/src/lib/utils.ts @@ -27,19 +27,14 @@ export function cn(...inputs: ClassValue[]): string { export const stripInlineStyles = (html: string): string => { if (!html) return html; - let cleanHtml = html; + // Pre-strip style attributes from the raw string BEFORE DOMPurify parses it. + // DOMPurify internally uses innerHTML to parse HTML, which triggers CSP + // `style-src` violations at parse time — before FORBID_ATTR can strip them. + // The regex is O(n) safe: [^"]* and [^']* are negated classes bounded by + // fixed quote delimiters, so no backtracking can occur. + const preStripped = html.replace(/ style="[^"]*"| style='[^']*'/gi, ""); - // In browser environments, pre-strip style attributes using DOMParser. - // DOMParser creates an inert document that does NOT trigger CSP violations, - // unlike DOMPurify's internal innerHTML which fires style-src violations - // at parse time — before FORBID_ATTR can strip them. - if (typeof DOMParser !== "undefined") { - const doc = new DOMParser().parseFromString(html, "text/html"); - doc.querySelectorAll("[style]").forEach((el) => el.removeAttribute("style")); - cleanHtml = doc.body.innerHTML; - } - - return DOMPurify.sanitize(cleanHtml, { + return DOMPurify.sanitize(preStripped, { FORBID_ATTR: ["style"], ADD_ATTR: ["target"], KEEP_CONTENT: true, diff --git a/packages/surveys/src/lib/html-utils.ts b/packages/surveys/src/lib/html-utils.ts index 187899342a..a58771b3f9 100644 --- a/packages/surveys/src/lib/html-utils.ts +++ b/packages/surveys/src/lib/html-utils.ts @@ -10,19 +10,14 @@ import DOMPurify from "isomorphic-dompurify"; export const stripInlineStyles = (html: string): string => { if (!html) return html; - let cleanHtml = html; + // Pre-strip style attributes from the raw string BEFORE DOMPurify parses it. + // DOMPurify internally uses innerHTML to parse HTML, which triggers CSP + // `style-src` violations at parse time — before FORBID_ATTR can strip them. + // The regex is O(n) safe: [^"]* and [^']* are negated classes bounded by + // fixed quote delimiters, so no backtracking can occur. + const preStripped = html.replace(/ style="[^"]*"| style='[^']*'/gi, ""); - // In browser environments, pre-strip style attributes using DOMParser. - // DOMParser creates an inert document that does NOT trigger CSP violations, - // unlike DOMPurify's internal innerHTML which fires style-src violations - // at parse time — before FORBID_ATTR can strip them. - if (typeof DOMParser !== "undefined") { - const doc = new DOMParser().parseFromString(html, "text/html"); - doc.querySelectorAll("[style]").forEach((el) => el.removeAttribute("style")); - cleanHtml = doc.body.innerHTML; - } - - return DOMPurify.sanitize(cleanHtml, { + return DOMPurify.sanitize(preStripped, { FORBID_ATTR: ["style"], ADD_ATTR: ["target"], KEEP_CONTENT: true,