From 76b2bc86d7721a3b6c41c4161a0dd15e91beeff4 Mon Sep 17 00:00:00 2001 From: NoxzRCW <115419063+NoxzRCW@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:14:39 +0200 Subject: [PATCH 1/2] fix(dashboard): guard provider icon lookups against prototype collisions getLobeProviderIcon indexed two object literals with no own-property check. A provider id whose lowercased form is an Object.prototype member resolves through the prototype chain: LOBE_PROVIDER_ALIASES["constructor"] returns the Object constructor, which is truthy, so the existing falsy guard lets it through. The follow-up LOBE_ICON_COMPONENTS lookup is then undefined and entry.color throws. ProviderIcon calls this for every card, so one such provider id takes the whole providers page down through the App Router error boundary, which shows a "check your connection" card even though nothing was wrong with the network. Only constructor and __proto__ can reach it. Every other Object.prototype member is camelCase and stops colliding after toLowerCase(). Closes #11853 --- src/shared/components/lobeProviderIcons.ts | 12 ++++- ...er-icons-prototype-collision-11853.test.ts | 48 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 tests/unit/lobe-provider-icons-prototype-collision-11853.test.ts diff --git a/src/shared/components/lobeProviderIcons.ts b/src/shared/components/lobeProviderIcons.ts index 5cc463d98c5..c45a07cb1a8 100644 --- a/src/shared/components/lobeProviderIcons.ts +++ b/src/shared/components/lobeProviderIcons.ts @@ -484,9 +484,17 @@ export function getLobeProviderIcon( providerId: string, type: "mono" | "color" = "color" ): LobeIconComponent | null { - const iconKey = LOBE_PROVIDER_ALIASES[providerId.toLowerCase()]; - if (!iconKey) return null; + if (typeof providerId !== "string") return null; + const aliasKey = providerId.toLowerCase(); + // Own-property guards: a providerId such as "constructor" or "__proto__" + // otherwise resolves through Object.prototype, yielding a truthy iconKey + // whose LOBE_ICON_COMPONENTS lookup is undefined -> `entry.color` throws and + // takes down the whole providers dashboard via the error boundary. + if (!Object.hasOwn(LOBE_PROVIDER_ALIASES, aliasKey)) return null; + const iconKey = LOBE_PROVIDER_ALIASES[aliasKey]; + if (!iconKey || !Object.hasOwn(LOBE_ICON_COMPONENTS, iconKey)) return null; const entry = LOBE_ICON_COMPONENTS[iconKey]; + if (!entry) return null; return type === "color" && entry.color ? entry.color : entry.mono; } diff --git a/tests/unit/lobe-provider-icons-prototype-collision-11853.test.ts b/tests/unit/lobe-provider-icons-prototype-collision-11853.test.ts new file mode 100644 index 00000000000..0e19fe8892f --- /dev/null +++ b/tests/unit/lobe-provider-icons-prototype-collision-11853.test.ts @@ -0,0 +1,48 @@ +/** + * Regression for #11853 — the providers dashboard crashed with + * "Cannot read properties of undefined (reading 'color')" and rendered a + * misleading "Failed to load providers — check your connection" card. + * + * `getLobeProviderIcon()` indexed two plain object literals without own-property + * guards. A provider id whose lowercased form is an Object.prototype member + * resolves through the prototype chain: `LOBE_PROVIDER_ALIASES["constructor"]` + * returns the Object constructor (truthy, so the `if (!iconKey) return null` + * guard passes), then `LOBE_ICON_COMPONENTS[]` is undefined and + * `entry.color` throws — taking the whole page down through the App Router + * error boundary, since ProviderIcon calls this for every provider card. + * + * Only `constructor` and `__proto__` are reachable: every other Object.prototype + * member is camelCase and no longer collides after `.toLowerCase()`. + * + * Runner: node --import tsx/esm --test tests/unit/lobe-provider-icons-prototype-collision-11853.test.ts + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { getLobeProviderIcon } = await import("../../src/shared/components/lobeProviderIcons.ts"); + +test("#11853 — prototype-colliding provider ids return null instead of throwing", () => { + for (const id of ["constructor", "__proto__", "CONSTRUCTOR", "__PROTO__"]) { + for (const type of ["color", "mono"] as const) { + assert.doesNotThrow(() => getLobeProviderIcon(id, type), `${id} (${type}) must not throw`); + assert.equal(getLobeProviderIcon(id, type), null, `${id} (${type}) must resolve to null`); + } + } +}); + +test("#11853 — camelCase prototype members were already safe and stay safe", () => { + for (const id of ["valueOf", "toString", "hasOwnProperty", "isPrototypeOf"]) { + assert.equal(getLobeProviderIcon(id), null); + } +}); + +test("#11853 — no regression: known providers still resolve, unknown ones still null", () => { + assert.notEqual(getLobeProviderIcon("openai"), null); + assert.notEqual(getLobeProviderIcon("anthropic"), null); + assert.equal(getLobeProviderIcon("definitely-not-a-provider"), null); +}); + +test("#11853 — a non-string provider id does not throw", () => { + assert.doesNotThrow(() => getLobeProviderIcon(undefined as unknown as string)); + assert.equal(getLobeProviderIcon(undefined as unknown as string), null); +}); From 2e5ca3a8fb096350fc76477a904d9c011e18550a Mon Sep 17 00:00:00 2001 From: NoxzRCW <115419063+NoxzRCW@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:17:54 +0200 Subject: [PATCH 2/2] docs(changelog): add fragment for #11880 --- changelog.d/fixes/11880-provider-icon-prototype-collision.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/fixes/11880-provider-icon-prototype-collision.md diff --git a/changelog.d/fixes/11880-provider-icon-prototype-collision.md b/changelog.d/fixes/11880-provider-icon-prototype-collision.md new file mode 100644 index 00000000000..493608fb8ac --- /dev/null +++ b/changelog.d/fixes/11880-provider-icon-prototype-collision.md @@ -0,0 +1 @@ +- **fix(dashboard):** the providers page no longer crashes into the error boundary when a provider id collides with an `Object.prototype` member (`constructor`, `__proto__`); icon lookups are own-property guarded ([#11880](https://github.com/diegosouzapw/OmniRoute/pull/11880)) — thanks @NoxzRCW