From d9368ec051a151dc377ec2daa98d7372ab4a1e86 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Wed, 8 Jul 2026 02:34:18 -0700 Subject: [PATCH] =?UTF-8?q?fix(core):=20address=20PR=20feedback=20?= =?UTF-8?q?=E2=80=94=20ReDoS-safe=20slug=20trim,=20getVariables=20cleanups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - slugify: replace the anchored alternated trim regex (/^-+|-+$/g) with a character-scan trim — CodeQL js/polynomial-redos blocker. - readRenderOverrides: fold the readOverrides wrapper into the exported function (one name, no pass-through). - getVariables: deduplicate declarers with a Set, matching injectCompositionCssVariables. - Move the tokenSlug import to the top of the file. Co-Authored-By: Claude Fable 5 --- packages/core/src/runtime/getVariables.ts | 21 ++++++++++++--------- packages/core/src/tokenSlug.ts | 13 +++++++++---- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/core/src/runtime/getVariables.ts b/packages/core/src/runtime/getVariables.ts index 367857052..546e688b6 100644 --- a/packages/core/src/runtime/getVariables.ts +++ b/packages/core/src/runtime/getVariables.ts @@ -19,6 +19,8 @@ * * const { title = "Untitled", theme = "light" } = getVariables(); */ +import { cssVariableName, detectSlugCollisions } from "../tokenSlug"; + export function getVariables< T extends Record = Record, >(): Partial { @@ -27,11 +29,18 @@ export function getVariables< // Same collection the CSS-variable injection uses: first, then any // composition element carrying the attribute (later declarers win), then // render-time overrides. - const declaredDefaults = readDeclaredDefaults(document.documentElement); + const declarers = new Set(); + if (document.documentElement?.hasAttribute("data-composition-variables")) { + declarers.add(document.documentElement); + } for (const el of Array.from(document.querySelectorAll("[data-composition-variables]"))) { + declarers.add(el); + } + const declaredDefaults: Record = {}; + for (const el of declarers) { Object.assign(declaredDefaults, readDeclaredDefaults(el)); } - const overrides = readOverrides(); + const overrides = readRenderOverrides(); return { ...declaredDefaults, ...overrides } as Partial; } @@ -65,8 +74,6 @@ export function readDeclaredDefaults(root: Element | null): Record /** Render-time variable overrides (`hyperframes render --variables`). */ export function readRenderOverrides(): Record { - return readOverrides(); -} - -function readOverrides(): Record { if (typeof window === "undefined") return {}; const raw = (window as Window & { __hfVariables?: unknown }).__hfVariables; if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}; diff --git a/packages/core/src/tokenSlug.ts b/packages/core/src/tokenSlug.ts index d641d6fed..653773090 100644 --- a/packages/core/src/tokenSlug.ts +++ b/packages/core/src/tokenSlug.ts @@ -8,10 +8,15 @@ */ export function slugify(name: string): string { - const slug = name - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); + const collapsed = name.toLowerCase().replace(/[^a-z0-9]+/g, "-"); + // Character-scan trim of leading/trailing "-" instead of /^-+|-+$/: + // CodeQL flags the alternated anchored regex as polynomial ReDoS on + // adversarial inputs (js/polynomial-redos). + let start = 0; + let end = collapsed.length; + while (start < end && collapsed[start] === "-") start++; + while (end > start && collapsed[end - 1] === "-") end--; + const slug = collapsed.slice(start, end); return slug.length > 0 ? slug : "node"; }