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
+9 -3
View File
@@ -1,7 +1,7 @@
import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr, safeUrl, scopeId } from '../../utils/escape';
import { escapeHtml, escapeAttr, safeUrl, scopeId, cssValue } from '../../utils/escape';
interface GalleryImage {
src: string;
@@ -130,8 +130,14 @@ Gallery.craft = {
...props.style,
});
const images = props.images || defaultImages;
const columns = props.columns || 3;
const gap = props.gap || '16px';
// Number() coercion: `columns` is a raw string-interpolation sink into the
// grid style attribute below (repeat(${columns},1fr)) -- a non-numeric
// (e.g. hand-crafted/AI-generated tree) value would otherwise be able to
// break out; Number() of anything non-numeric collapses safely to NaN.
const columns = Number(props.columns) || 3;
// Sanitized -- gap is a raw string-interpolation sink into the grid style
// attribute below.
const gap = cssValue(props.gap) || '16px';
const lightbox = props.lightbox || false;
// Deterministic AND unique id, scoped on the Craft node id, for this