Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
12 changes: 10 additions & 2 deletions src/shared/components/lobeProviderIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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[<that function>]` 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);
});