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
parent 5acf172511
commit 36c3b2f503
24 changed files with 318 additions and 70 deletions
+10 -6
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, scopeId } from '../../utils/escape';
import { escapeHtml, escapeAttr, scopeId, cssValue } from '../../utils/escape';
interface TabItem {
label: string;
@@ -131,11 +131,15 @@ Tabs.craft = {
});
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
const tabs = props.tabs || defaultTabs;
const activeTabBg = props.activeTabBg || '#3b82f6';
const activeTabColor = props.activeTabColor || '#ffffff';
const inactiveTabBg = props.inactiveTabBg || '#f1f5f9';
const inactiveTabColor = props.inactiveTabColor || '#64748b';
const contentBg = props.contentBg || '#ffffff';
// Sanitized -- raw string-interpolation sinks below, both into style="..."
// attributes AND into an inline <script> as single-quoted JS string
// literals (a stray `'` there breaks out of the JS string, not just CSS);
// cssValue strips quotes too so it neutralizes both contexts at once.
const activeTabBg = cssValue(props.activeTabBg) || '#3b82f6';
const activeTabColor = cssValue(props.activeTabColor) || '#ffffff';
const inactiveTabBg = cssValue(props.inactiveTabBg) || '#f1f5f9';
const inactiveTabColor = cssValue(props.inactiveTabColor) || '#64748b';
const contentBg = cssValue(props.contentBg) || '#ffffff';
// tabId scopes the functional wiring (onclick/getElementById) as well as
// the ARIA tab<->panel linking ids. It must be BOTH deterministic (so