diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index 1acb57b4..e855442f 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -167,7 +167,17 @@ 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)) { + // Restrict to real token-name keys too, not just string values — an + // imported JSON file is untrusted input and its keys end up as + // object property names downstream (CodeQL: remote-property-injection). + const safe = Object.fromEntries( + Object.entries(data as Record).filter( + ([k, v]) => typeof v === "string" && /^--sf-[\w-]+$/.test(k) + ) + ) as Record; + if (Object.keys(safe).length > 0) 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..5031d616 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,25 @@ // 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"); + const darkSource = sourceByName("--sf-color-base-source-dark"); + const hasExplicitDark = darkSource && (overrides[darkSource.name] ?? sourceTokenMap[darkSource.name]?.value) !== undefined; + if (hasExplicitDark) return sourceValue(darkSource); + 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 +535,7 @@ {@const resolved = paletteSwatch(light.colorKey, lightSrcVal, step, lSurface, lText)}
{/each} @@ -543,7 +548,7 @@ {@const resolved = paletteSwatch(light.colorKey, darkSrcVal, step, dSurface, dText)}
{/each} @@ -570,7 +575,7 @@
Dark: auto-derived ({derivedDark}) @@ -697,7 +702,7 @@
Dark: auto-derived ({derivedDark}) @@ -718,7 +723,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/codec.ts b/configurator/src/lib/codec.ts index e96cb47e..02bfc29e 100644 --- a/configurator/src/lib/codec.ts +++ b/configurator/src/lib/codec.ts @@ -186,7 +186,12 @@ export function da(e: string | null | undefined): string { export function fa(e: Record, t: { mode?: "layer" | "root"; banner?: boolean } = {}): string { const { mode = "layer", banner = true } = t; - const i = Object.keys(e).sort((a, b) => a.localeCompare(b)); + // Keys land here from several untrusted-ish entry points (imported JSON, + // shared URL hash, localStorage, WP hydration) — unlike values, they were + // never sanitized before being interpolated into the emitted CSS, so a + // crafted key could break out of its declaration. Every real token name + // (source or derived) is --sf-; anything else is dropped. + const i = Object.keys(e).filter((key) => /^--sf-[\w-]+$/.test(key)).sort((a, b) => a.localeCompare(b)); if (i.length === 0) return ""; const a = i.map((key) => `${key}: ${da(e[key])};`); 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;