diff --git a/src/theme/__tests__/provider.test.ts b/src/theme/__tests__/provider.test.ts index acac88b491..b1f421be36 100644 --- a/src/theme/__tests__/provider.test.ts +++ b/src/theme/__tests__/provider.test.ts @@ -124,6 +124,49 @@ describe('safeMerge', () => { expect(result.colors.secondary).toBe('#999'); expect(result.colors.tertiary).toBe('#222'); }); + + it('ignores a top-level __proto__ key', () => { + const overrides = JSON.parse('{"__proto__": {"polluted": "yes"}, "a": 2}'); + + const result = safeMerge>({ a: 1 }, overrides); + expect(Object.getPrototypeOf(result)).toBe(Object.prototype); + expect(result).toEqual({ a: 2 }); + expect(result.polluted).toBeUndefined(); + const fresh: Record = {}; + expect(fresh.polluted).toBeUndefined(); + }); + + it('ignores a nested __proto__ key', () => { + const base = { colors: { primary: 'blue', secondary: 'green' } }; + const overrides = JSON.parse( + '{"colors": {"__proto__": {"polluted": "yes"}, "primary": "red"}}' + ); + + const result = safeMerge<{ colors: Record }>( + base, + overrides + ); + expect(Object.getPrototypeOf(result.colors)).toBe(Object.prototype); + expect(result.colors.primary).toBe('red'); + expect(result.colors.secondary).toBe('green'); + expect(result.colors.polluted).toBeUndefined(); + }); + + it('skips constructor and prototype keys', () => { + const overrides = JSON.parse( + '{"constructor": {"x": 1}, "prototype": {"y": 2}, "a": 2}' + ); + + const result = safeMerge>({ a: 1 }, overrides); + expect(Object.prototype.hasOwnProperty.call(result, 'constructor')).toBe( + false + ); + expect(Object.prototype.hasOwnProperty.call(result, 'prototype')).toBe( + false + ); + expect(result.constructor).toBe(Object); + expect(result).toEqual({ a: 2 }); + }); }); describe('useInternalTheme', () => { diff --git a/src/theme/provider.tsx b/src/theme/provider.tsx index 2c1f8ca9f9..11d697bd19 100644 --- a/src/theme/provider.tsx +++ b/src/theme/provider.tsx @@ -29,6 +29,8 @@ export const isPlatformColorSentinel = (v: unknown): boolean => typeof v === 'object' && ('resource_paths' in v || 'semantic' in v || 'dynamic' in v); +const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']); + /* eslint-disable @typescript-eslint/no-unsafe-type-assertion */ export const safeMerge = (base: T, overrides: unknown): T => { if ( @@ -46,6 +48,9 @@ export const safeMerge = (base: T, overrides: unknown): T => { } const out: Record = { ...(base as Record) }; for (const key of Object.keys(overrides)) { + if (UNSAFE_KEYS.has(key)) { + continue; + } out[key] = safeMerge( (base as Record)[key], (overrides as Record)[key]