Compare commits

...

1 Commits

Author SHA1 Message Date
Markus Hartung
0517eaf902 fix(ui): guard remaining ProviderIcon lookups against prototype collisions (#11920 port) 2026-08-28 16:17:42 -03:00
4 changed files with 34 additions and 4 deletions

View File

@@ -0,0 +1 @@
- **fix(ui):** `ProviderIcon`'s three remaining unguarded lookups (`PROVIDER_ICON_ALIASES`, `LOCAL_SVG_ALIASES`, `THEMED_SVGS`) now use `Object.hasOwn()` own-property checks — a provider id such as `constructor` or `__proto__` previously resolved through the prototype chain instead of falling through to the unknown-provider CDN fallback (`getLobeProviderIcon()` itself was already guarded by [#11880](https://github.com/diegosouzapw/OmniRoute/pull/11880)); `ProviderPageHeader`'s `color` field is also now optional, matching the rest of the component's defensive typing (ported from [#11920](https://github.com/diegosouzapw/OmniRoute/pull/11920) — thanks @HouMinXi).

View File

@@ -12,7 +12,7 @@ interface ProviderInfo {
id: string;
name: string;
website?: string;
color: string;
color?: string;
apiType?: string;
/** Optional operator-supplied remote icon URL (#2166) for compatible provider nodes. */
iconUrl?: string;

View File

@@ -335,11 +335,19 @@ const ProviderIcon = memo(function ProviderIcon({
fallbackColor,
}: ProviderIconProps) {
const { isDark } = useTheme();
const normalizedId = PROVIDER_ICON_ALIASES[providerId.toLowerCase()] || providerId.toLowerCase();
const localSvgId = LOCAL_SVG_ALIASES[normalizedId] || normalizedId;
// Own-property guards: a providerId such as "constructor" or "__proto__" otherwise
// resolves through Object.prototype, yielding a truthy-looking value that corrupts
// downstream lookups instead of falling through to the unknown-provider path (#11853).
const providerIdLower = providerId.toLowerCase();
const normalizedId = Object.hasOwn(PROVIDER_ICON_ALIASES, providerIdLower)
? PROVIDER_ICON_ALIASES[providerIdLower]
: providerIdLower;
const localSvgId = Object.hasOwn(LOCAL_SVG_ALIASES, normalizedId)
? LOCAL_SVG_ALIASES[normalizedId]
: normalizedId;
const usesGenericIcon =
GENERIC_PROVIDER_IDS.has(normalizedId) || GENERIC_PROVIDER_IDS.has(localSvgId);
const themedSvg = THEMED_SVGS[normalizedId];
const themedSvg = Object.hasOwn(THEMED_SVGS, normalizedId) ? THEMED_SVGS[normalizedId] : undefined;
const hasSvg = KNOWN_SVGS.has(localSvgId);
const [failedAssets, setFailedAssets] = useState<Record<string, true>>({});

View File

@@ -243,3 +243,24 @@ describe("ProviderIcon — unresolved local asset provenance", () => {
}
);
});
// #11853 follow-up: getLobeProviderIcon() itself is already guarded by #11880's
// Object.hasOwn() checks (see lobe-provider-icons-prototype-collision-11853.test.ts).
// This covers the three *other* plain-object lookups ProviderIcon.tsx does on its own
// (PROVIDER_ICON_ALIASES, LOCAL_SVG_ALIASES, THEMED_SVGS) — none of which #11880 touched —
// which resolved the same inherited-property ids through the prototype chain before
// falling through to the thesvg.org unknown-provider CDN path.
describe("ProviderIcon — inherited object property ids", () => {
it.each(["constructor", "valueOf", "hasOwnProperty", "__proto__"])(
"renders provider id %s through the unknown-provider fallback",
(providerId) => {
const container = renderIcon({ providerId });
const img = container.querySelector("img");
expect(img).not.toBeNull();
expect(img?.getAttribute("src")).toBe(
`https://thesvg.org/icons/${providerId.toLowerCase()}/default.svg`
);
}
);
});