diff --git a/src/blobbi/companion/components/OverstimulationBlockOverlay.tsx b/src/blobbi/companion/components/OverstimulationBlockOverlay.tsx index 1697d7b0..7f2e0e16 100644 --- a/src/blobbi/companion/components/OverstimulationBlockOverlay.tsx +++ b/src/blobbi/companion/components/OverstimulationBlockOverlay.tsx @@ -40,8 +40,9 @@ export function OverstimulationBlockOverlay({ isBlocked }: Props) { // Find Blobbi's true visual center via DOM query (after scroll reset) const el = document.querySelector('[data-blobbi-companion]'); - const cx = el ? el.getBoundingClientRect().left + el.getBoundingClientRect().width / 2 : window.innerWidth / 2; - const cy = el ? el.getBoundingClientRect().top + el.getBoundingClientRect().height / 2 : window.innerHeight / 2; + const rect = el?.getBoundingClientRect(); + const cx = rect ? rect.left + rect.width / 2 : window.innerWidth / 2; + const cy = rect ? rect.top + rect.height / 2 : window.innerHeight / 2; origin.current = { x: cx, y: cy }; root.style.transformOrigin = `${cx}px ${cy}px`; diff --git a/src/blobbi/ui/lib/bodyEffects/generators.ts b/src/blobbi/ui/lib/bodyEffects/generators.ts index 55cc06d5..a1be645b 100644 --- a/src/blobbi/ui/lib/bodyEffects/generators.ts +++ b/src/blobbi/ui/lib/bodyEffects/generators.ts @@ -34,6 +34,25 @@ import type { BodyPathInfo, } from './types'; +// ─── SVG Color Sanitization ─────────────────────────────────────────────────── + +/** Hex color: #rgb, #rrggbb, or #rrggbbaa */ +const HEX_RE = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i; +/** Named CSS color or functional notation (rgb/rgba/hsl/hsla with digits, commas, spaces, dots, %) */ +const FUNC_RE = /^(?:rgb|rgba|hsl|hsla)\(\s*[\d.,\s%]+\)$/i; +const NAMED_RE = /^[a-z]{3,24}$/i; + +/** + * Validate a color value before interpolating it into SVG markup. + * Returns the color unchanged if it looks safe, otherwise returns a + * harmless fallback. This prevents SVG injection if a future caller + * ever passes user-controlled color strings. + */ +function sanitizeSvgColor(color: string, fallback = '#000'): string { + if (HEX_RE.test(color) || FUNC_RE.test(color) || NAMED_RE.test(color)) return color; + return fallback; +} + // ─── Body Path Detection ────────────────────────────────────────────────────── /** @@ -728,6 +747,7 @@ export function generateAngerRiseEffect( const { pathD, minX, maxX, minY, maxY } = bodyPath; const bodyHeight = maxY - minY; const bodyWidth = maxX - minX; + const safeColor = sanitizeSvgColor(config.color); const suffix = idSuffix ?? Math.random().toString(36).slice(2, 8); const clipId = `blobbi-anger-clip-${suffix}`; @@ -759,16 +779,16 @@ export function generateAngerRiseEffect( - - - + + + ` : ` - + - + - +