fix(builder): sanitize CSS-value sinks to prevent style/<style> breakout XSS

Adds a single cssValue() sanitizer (src/utils/escape.ts) that strips
<>{};"'\ and neutralizes url(), safe for both style="..." attribute and
<style>...</style> element contexts. Applies it at every raw user-prop
CSS-value interpolation sink found via grep across src/components (colors,
sizes, gaps interpolated directly into style strings/<style> blocks),
including the highest-risk <style>-context sinks: ColumnLayout gap,
Menu/Navbar hover and background colors. Also Number()-coerces the
`columns` grid-template-columns sinks in Gallery/Testimonials/NumberCounter
as defense in depth. Regression tests assert </style><script> payloads are
neutralized and normal colors still render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 15:56:25 -07:00
co-authored by Claude Opus 4.8
parent 5acf172511
commit 36c3b2f503
24 changed files with 318 additions and 70 deletions
+7 -5
View File
@@ -1,7 +1,7 @@
import React, { CSSProperties, useState } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr } from '../../utils/escape';
import { escapeHtml, escapeAttr, cssValue } from '../../utils/escape';
interface AccordionItem {
title: string;
@@ -151,10 +151,12 @@ Accordion.craft = {
...props.style,
});
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
const headerBg = props.headerBg || '#f8fafc';
const headerColor = props.headerColor || '#18181b';
const contentBg = props.contentBg || '#ffffff';
const borderColor = props.borderColor || '#e2e8f0';
// Sanitized -- raw string-interpolation sinks in the <details>/<summary>
// style attributes below.
const headerBg = cssValue(props.headerBg) || '#f8fafc';
const headerColor = cssValue(props.headerColor) || '#18181b';
const contentBg = cssValue(props.contentBg) || '#ffffff';
const borderColor = cssValue(props.borderColor) || '#e2e8f0';
const items = props.items || defaultItems;
const panels = items.map((item, i) => {