From 14b295f545c422f229ad5d3a596ca0fbef6e9d41 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 21:16:43 +0000 Subject: [PATCH 1/4] fix(configurator): null-safe JSON import, finite-number guards, style-injection hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugs surfaced by automated review of SLASHED-Plugins#122 (which vendors this tree verbatim) that predate that PR but were never caught here since this repo has no CodeQL/CodeRabbit coverage of its own: - App.svelte: handleImport() passed any parsed JSON value straight to setOverrides(), including null/arrays — typeof null === "object", so a malformed import file crashed the configurator in shallowEq's Object.keys() call. Restored the data!==null/!Array.isArray/string-value filtering that AppOverlay.svelte's import flow already had. - lib/persistence.ts: getNum()'s isNaN() guard let Infinity/-Infinity through (parseFloat can return both), which fmt() then serialized as the literal strings "Infinity"/"NaN" into generated CSS. Switched to Number.isFinite() and added a matching guard in fmt() itself. - ColorsPanel.svelte: getLightSurface/getDarkSurface/getLightText/ getDarkText read overrides[...] directly instead of going through sourceValue()'s override→loaded-token→default precedence, so the palette swatch strips could disagree with the rest of the panel when a source token was synced/loaded but not overridden. Added sourceByName() and routed all four through sourceValue(). - EffectsPanel.svelte: the text-shadow input checked v.trim() but passed the untrimmed v to onSet, so whitespace-padded values were persisted. - MotionPanel.svelte: staggerBase was $derived to a function instead of a value ($derived(() => ...) instead of $derived.by(() => ...)), so it never recomputed when overrides/scale changed. - SpacingPanel.svelte: the space-scale preview's exponent offset was off-by-one (i - 4 instead of i - 3) — SPACE_STEPS[3] is "m", the framework's exponent-0 anchor (core/tokens.css), so every preview bar rendered one step below its real fluid-scale value. - ThemesPanel.svelte + the swatch/preview sites across ColorsPanel, EffectsPanel, TokenRow, ColorInput, and OklchColorDesk: color values (sourced from overrides, which can come from a saved theme, imported JSON, or a shared URL hash) were interpolated into raw `style={...}` template strings. A value like `red; background-image:url(...)` would inject extra CSS declarations. Switched the single-value cases to Svelte's `style:background={value}` property directive, which only ever sets that one property — no declaration-separator injection possible. svelte-check (0 errors) and vite build both pass. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SQXr34nocCi1jrGcp5TPVm --- configurator/src/App.svelte | 7 +++++- .../src/components/inputs/ColorInput.svelte | 2 +- .../components/inputs/OklchColorDesk.svelte | 2 +- .../src/components/inputs/TokenRow.svelte | 2 +- .../src/components/panels/ColorsPanel.svelte | 22 +++++++++++-------- .../src/components/panels/EffectsPanel.svelte | 7 +++--- .../src/components/panels/MotionPanel.svelte | 6 ++--- .../src/components/panels/SpacingPanel.svelte | 2 +- .../src/components/panels/ThemesPanel.svelte | 2 +- configurator/src/lib/persistence.ts | 3 ++- 10 files changed, 33 insertions(+), 22 deletions(-) diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index 1acb57b4..1a33455a 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -167,7 +167,12 @@ if (file.name.endsWith(".json")) { try { const data = JSON.parse(text); - if (typeof data === "object") setOverrides(data); + if (data !== null && typeof data === "object" && !Array.isArray(data)) { + const safe = Object.fromEntries( + Object.entries(data as Record).filter(([, v]) => typeof v === "string") + ) as Record; + setOverrides(safe); + } } catch {} } else { const parsed: Record = {}; diff --git a/configurator/src/components/inputs/ColorInput.svelte b/configurator/src/components/inputs/ColorInput.svelte index d4b82f01..1a927f52 100644 --- a/configurator/src/components/inputs/ColorInput.svelte +++ b/configurator/src/components/inputs/ColorInput.svelte @@ -62,7 +62,7 @@
-
+
{#if !isVar}
{label}
diff --git a/configurator/src/components/inputs/TokenRow.svelte b/configurator/src/components/inputs/TokenRow.svelte index fb90767d..9807e872 100644 --- a/configurator/src/components/inputs/TokenRow.svelte +++ b/configurator/src/components/inputs/TokenRow.svelte @@ -39,7 +39,7 @@ {#if type === "color"}
{:else}
diff --git a/configurator/src/components/panels/ColorsPanel.svelte b/configurator/src/components/panels/ColorsPanel.svelte index b5a91f36..7184f1da 100644 --- a/configurator/src/components/panels/ColorsPanel.svelte +++ b/configurator/src/components/panels/ColorsPanel.svelte @@ -304,6 +304,10 @@ return overrides[source.name] ?? sourceTokenMap[source.name]?.value ?? source.default; } + function sourceByName(name: string): ColorSource | undefined { + return ALL_SOURCES.find((s) => s.name === name); + } + let activeCurvePreset = $derived(CURVE_PRESETS.find((p) => Object.entries(p.patch).every(([k, v]) => v === null ? !(k in overrides) : overrides[k] === v @@ -343,24 +347,24 @@ // Endpoint colors for palette step computation — approximated from source tokens so we // can build concrete color-mix() expressions without relying on the themed probe. function getLightSurface(): string { - return overrides["--sf-color-base-source-light"] ?? "oklch(0.96 0.006 250)"; + return sourceValue(sourceByName("--sf-color-base-source-light")); } function getDarkSurface(): string { const custom = overrides["--sf-color-base-source-dark"]; if (custom) return custom; - return deriveDarkFromLight(overrides["--sf-color-base-source-light"] ?? "oklch(0.96 0.006 250)", "base"); + return deriveDarkFromLight(sourceValue(sourceByName("--sf-color-base-source-light")), "base"); } function getLightText(): string { - const n = overrides["--sf-color-neutral-source-light"] ?? "oklch(0.52 0.025 260)"; + const n = sourceValue(sourceByName("--sf-color-neutral-source-light")); const { l, c, h, valid } = parseOklch(n); if (!valid) return "oklch(0.12 0.02 260)"; return stringifyOklch(Math.max(0.05, Math.min(l - 0.38, 0.3)), c * 0.8, h); } function getDarkText(): string { - const n = overrides["--sf-color-neutral-source-dark"] ?? "oklch(0.69 0.0225 260)"; + const n = sourceValue(sourceByName("--sf-color-neutral-source-dark")); const { l, c, h, valid } = parseOklch(n); if (!valid) return "oklch(0.92 0.02 260)"; return stringifyOklch(Math.min(1.0, Math.max(l + 0.22, 0.88)), c * 0.8, h); @@ -530,7 +534,7 @@ {@const resolved = paletteSwatch(light.colorKey, lightSrcVal, step, lSurface, lText)}
{/each} @@ -543,7 +547,7 @@ {@const resolved = paletteSwatch(light.colorKey, darkSrcVal, step, dSurface, dText)}
{/each} @@ -570,7 +574,7 @@
Dark: auto-derived ({derivedDark}) @@ -697,7 +701,7 @@
Dark: auto-derived ({derivedDark}) @@ -718,7 +722,7 @@ {@const resolved = computePaletteSwatch(srcVal as string, step, sfc as string, txt as string)}
{/each} diff --git a/configurator/src/components/panels/EffectsPanel.svelte b/configurator/src/components/panels/EffectsPanel.svelte index 40dd6d25..9f565690 100644 --- a/configurator/src/components/panels/EffectsPanel.svelte +++ b/configurator/src/components/panels/EffectsPanel.svelte @@ -228,8 +228,8 @@ {/each}
-
-
+
+
Scrollbar preview
@@ -286,7 +286,8 @@ placeholder={t.default} oninput={(e) => { const v = (e.target as HTMLInputElement).value; - v.trim() ? onSet(t.token, v) : onReset(t.token); + const trimmed = v.trim(); + trimmed ? onSet(t.token, trimmed) : onReset(t.token); }} class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded px-1.5 py-1 text-[9px] font-mono text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500" /> diff --git a/configurator/src/components/panels/MotionPanel.svelte b/configurator/src/components/panels/MotionPanel.svelte index b24297da..1157fea3 100644 --- a/configurator/src/components/panels/MotionPanel.svelte +++ b/configurator/src/components/panels/MotionPanel.svelte @@ -36,7 +36,7 @@ let scale = $derived((() => { const v = parseFloat(overrides["--sf-motion-scale"] ?? "1"); return isFinite(v) ? v : 1; })()); let motionDisabled = $derived(overrides["--sf-motion-scale"] === "0"); let themeTransition = $derived((() => { const v = parseFloat(overrides["--sf-theme-transition-duration"]?.replace("ms","") ?? String(300 * scale)); return isFinite(v) ? v : Math.round(300 * scale); })()); - let staggerBase = $derived(() => { + let staggerBase = $derived.by(() => { const raw = overrides[STAGGER_TOKENS[0]]; if (raw) return parseFloat(raw.replace("ms","")); return 75 * scale; @@ -292,7 +292,7 @@ {/if}
t in overrides)} onChange={(v) => setStaggerBase(v)} @@ -301,7 +301,7 @@
{#each [1,2,3,4,5] as n (n)} - {@const delayMs = Math.round(staggerBase() * n)} + {@const delayMs = Math.round(staggerBase * n)}
–{n}
diff --git a/configurator/src/components/panels/SpacingPanel.svelte b/configurator/src/components/panels/SpacingPanel.svelte index 6c0c226b..1f988581 100644 --- a/configurator/src/components/panels/SpacingPanel.svelte +++ b/configurator/src/components/panels/SpacingPanel.svelte @@ -55,7 +55,7 @@ {#each SPACE_STEPS as step, i (step)} {@const midBase = (baseMin + baseMax) / 2} {@const ratio = (ratioMin + ratioMax) / 2} - {@const offset = i - 4} + {@const offset = i - 3} {@const rawRem = offset >= 0 ? midBase * Math.pow(ratio, offset) : midBase / Math.pow(ratio, -offset)} {@const scaled = rawRem * spaceScale} {@const barWidth = Math.min(scaled * 28, 240)} diff --git a/configurator/src/components/panels/ThemesPanel.svelte b/configurator/src/components/panels/ThemesPanel.svelte index 8c3e5971..f972e078 100644 --- a/configurator/src/components/panels/ThemesPanel.svelte +++ b/configurator/src/components/panels/ThemesPanel.svelte @@ -114,7 +114,7 @@ {#each Object.entries(theme.overrides).slice(0, 5) as [k, v] (k)}
{#if k.includes("color") || k.includes("source")} -
+
{/if} {k.replace("--sf-", "")}
diff --git a/configurator/src/lib/persistence.ts b/configurator/src/lib/persistence.ts index e5cb0c83..29449e7f 100644 --- a/configurator/src/lib/persistence.ts +++ b/configurator/src/lib/persistence.ts @@ -67,10 +67,11 @@ function getNum(ov: Record, key: string, def: number): number { const v = ov[key]; if (v === undefined) return def; const n = parseFloat(v); - return isNaN(n) ? def : n; + return Number.isFinite(n) ? n : def; } function fmt(n: number): string { + if (!Number.isFinite(n)) return '0'; const s = n.toFixed(6); const trimmed = s.replace(/\.?0+$/, ''); return trimmed === '' || trimmed === '-' ? '0' : trimmed; From 9d631f07a750b2fd31bc4c2c475340f730970d3a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 21:26:22 +0000 Subject: [PATCH 2/4] fix(configurator): sanitize override keys before CSS emission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fa() interpolated override keys verbatim into generated CSS (`${key}: ${da(e[key])};`) while only sanitizing values through da(). Keys reach this function from several untrusted-ish entry points — imported JSON, the shareable URL hash, localStorage, WP hydration — none of which validate key shape, so a crafted key could break out of its CSS declaration in the live-preview