fix: pre-strip style attributes before DOMPurify to prevent CSP violations

DOMPurify internally uses innerHTML to parse HTML strings. When survey
content (headlines, descriptions) contains style attributes from the
rich text editor, the browser's HTML parser triggers CSP style-src
violations at parse time — before DOMPurify can strip them via
FORBID_ATTR.

Fix: use DOMParser to parse into an inert document (no CSP violations),
remove style attributes from the DOM tree, then pass the clean HTML to
DOMPurify for XSS sanitization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dhruwang
2026-03-17 15:39:51 +05:30
parent f65073bc81
commit 5587057bb3
2 changed files with 24 additions and 26 deletions

View File

@@ -27,20 +27,19 @@ export function cn(...inputs: ClassValue[]): string {
export const stripInlineStyles = (html: string): string => {
if (!html) return 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 if the input contains style attributes — even though
// FORBID_ATTR would remove them afterwards. By stripping them first, the
// browser's HTML parser never encounters them.
//
// ReDoS safety: all quantifiers (\s+, \s*, [^"]*,[^']*) are separated by
// fixed literals or are negated character classes — no two can compete over
// the same characters, so runtime is O(n). The length cap is defense-in-depth.
const MAX_HTML_LENGTH = 100_000;
const safeHtml = html.length > MAX_HTML_LENGTH ? html.slice(0, MAX_HTML_LENGTH) : html;
const preStripped = safeHtml.replace(/\s+style\s*=\s*(?:"[^"]*"|'[^']*')/gi, "");
let cleanHtml = html;
return DOMPurify.sanitize(preStripped, {
// 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, {
FORBID_ATTR: ["style"],
ADD_ATTR: ["target"],
KEEP_CONTENT: true,

View File

@@ -10,20 +10,19 @@ import DOMPurify from "isomorphic-dompurify";
export const stripInlineStyles = (html: string): string => {
if (!html) return 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 if the input contains style attributes — even though
// FORBID_ATTR would remove them afterwards. By stripping them first, the
// browser's HTML parser never encounters them.
//
// ReDoS safety: all quantifiers (\s+, \s*, [^"]*,[^']*) are separated by
// fixed literals or are negated character classes — no two can compete over
// the same characters, so runtime is O(n). The length cap is defense-in-depth.
const MAX_HTML_LENGTH = 100_000;
const safeHtml = html.length > MAX_HTML_LENGTH ? html.slice(0, MAX_HTML_LENGTH) : html;
const preStripped = safeHtml.replace(/\s+style\s*=\s*(?:"[^"]*"|'[^']*')/gi, "");
let cleanHtml = html;
return DOMPurify.sanitize(preStripped, {
// 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, {
FORBID_ATTR: ["style"],
ADD_ATTR: ["target"],
KEEP_CONTENT: true,