From 2453a53a7ce401fedc3205161fe2eaede46608c0 Mon Sep 17 00:00:00 2001 From: philluiz2323 Date: Wed, 10 Jun 2026 11:16:31 -0700 Subject: [PATCH] fix(signals): subject over-long manifest entries to dedup and list cap normalizeStringList truncated an over-long entry then continue'd, bypassing both the !result.includes de-dup and the MAX_LIST_ITEMS cap -- so untrusted manifests could produce duplicated and over-cap (201+ entries fit under the 64KB gate) wantedPaths/blockedPaths/etc. Truncate in place and fall through to the same de-dup and cap logic. --- src/signals/focus-manifest.ts | 11 +++++++---- test/unit/focus-manifest.test.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index c51f162d19..f5d44e9d0c 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -100,12 +100,15 @@ function normalizeStringList(value: JsonValue | undefined, field: string, warnin } const trimmed = entry.trim(); if (!trimmed) continue; - if (trimmed.length > MAX_ITEM_LENGTH) { + // Truncate in place, then flow through the same de-dup and cap logic. Falling through (rather than + // `continue`-ing) keeps over-long entries subject to both limits, so untrusted manifests cannot + // bypass de-duplication or the MAX_LIST_ITEMS safety cap via pathological long entries. + let normalized = trimmed; + if (normalized.length > MAX_ITEM_LENGTH) { warnings.push(`Manifest field "${field}" truncated an over-long entry.`); - result.push(trimmed.slice(0, MAX_ITEM_LENGTH)); - continue; + normalized = normalized.slice(0, MAX_ITEM_LENGTH); } - if (!result.includes(trimmed)) result.push(trimmed); + if (!result.includes(normalized)) result.push(normalized); if (result.length >= MAX_LIST_ITEMS) { warnings.push(`Manifest field "${field}" exceeded ${MAX_LIST_ITEMS} entries; extra entries ignored.`); break; diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index d38e443390..3e4761f3a2 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -82,6 +82,20 @@ describe("parseFocusManifest", () => { expect(manifest.wantedPaths).toEqual(["src/", "lib/"]); }); + it("de-duplicates over-long entries after truncation", () => { + const prefix = "a".repeat(300); + const manifest = parseFocusManifest({ wantedPaths: [`${prefix}X`, `${prefix}Y`] }); + expect(manifest.wantedPaths).toEqual([prefix]); + expect(manifest.warnings.join(" ")).toMatch(/truncated an over-long entry/); + }); + + it("applies the list cap to over-long entries", () => { + const overLong = Array.from({ length: 250 }, (_, index) => `path-${index}-${"x".repeat(300)}`); + const manifest = parseFocusManifest({ wantedPaths: overLong }); + expect(manifest.wantedPaths.length).toBe(200); + expect(manifest.warnings.join(" ")).toMatch(/exceeded 200 entries/); + }); + it("marks a manifest with no recognized fields as absent", () => { const manifest = parseFocusManifest({ unrelated: "value" }); expect(manifest.present).toBe(false);