From 8b0eb97abbc5bcdfd61103c7ac8ebaf4dfbff39f Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 23 Apr 2026 22:39:27 -0500 Subject: [PATCH] Fix redundant layout reflows and sanitize SVG color interpolation Cache getBoundingClientRect() result in OverstimulationBlockOverlay to avoid four forced reflows on the same element per activation. Add sanitizeSvgColor() guard in generateAngerRiseEffect() so that config.color is validated before interpolation into SVG stop-color attributes, preventing injection if a future caller passes untrusted color strings. --- .../OverstimulationBlockOverlay.tsx | 5 +-- src/blobbi/ui/lib/bodyEffects/generators.ts | 32 +++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) 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( - - - + + + ` : ` - + - + - +