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:
@@ -1,5 +1,5 @@
|
||||
import { CSSProperties } from 'react';
|
||||
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
||||
import { escapeHtml, escapeAttr, safeUrl, cssValue } from '../../utils/escape';
|
||||
|
||||
export type CtaVariant = 'primary' | 'outline' | 'ghost';
|
||||
|
||||
@@ -68,14 +68,21 @@ export function ctaInlineStyle(cta: CtaButton, defaults: CtaStyleDefaults): CSSP
|
||||
|
||||
export function ctaCssString(cta: CtaButton, defaults: CtaStyleDefaults): string {
|
||||
const variant = cta.variant || 'primary';
|
||||
// Sanitized -- these are raw string-interpolation sinks into style="...".
|
||||
// Callers pass user-controlled design-token colors (e.g. HeroSimple's
|
||||
// buttonBgColor/buttonTextColor/textColor) through CtaStyleDefaults, so
|
||||
// sanitize once here rather than at every call site.
|
||||
const outlineText = cssValue(defaults.outlineText) || '#000000';
|
||||
const primaryBg = cssValue(defaults.primaryBg) || '#000000';
|
||||
const primaryText = cssValue(defaults.primaryText) || '#ffffff';
|
||||
switch (variant) {
|
||||
case 'outline':
|
||||
return `display:inline-block;padding:14px 36px;background-color:transparent;color:${defaults.outlineText};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px;border:2px solid ${defaults.outlineText}`;
|
||||
return `display:inline-block;padding:14px 36px;background-color:transparent;color:${outlineText};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px;border:2px solid ${outlineText}`;
|
||||
case 'ghost':
|
||||
return `display:inline-block;padding:14px 24px;background-color:transparent;color:${defaults.outlineText};text-decoration:underline;border-radius:8px;font-weight:600;font-size:16px`;
|
||||
return `display:inline-block;padding:14px 24px;background-color:transparent;color:${outlineText};text-decoration:underline;border-radius:8px;font-weight:600;font-size:16px`;
|
||||
case 'primary':
|
||||
default:
|
||||
return `display:inline-block;padding:14px 36px;background-color:${defaults.primaryBg};color:${defaults.primaryText};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px`;
|
||||
return `display:inline-block;padding:14px 36px;background-color:${primaryBg};color:${primaryText};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user