diff --git a/packages/core/src/transfer_state.ts b/packages/core/src/transfer_state.ts index f6c98f85ad3..5d7376ad88a 100644 --- a/packages/core/src/transfer_state.ts +++ b/packages/core/src/transfer_state.ts @@ -48,6 +48,11 @@ export function makeStateKey(key: string): StateKey { return key as StateKey; } +function createDictionary(): Record { + // StateKey can be any string, including names of special Object prototype properties. + return Object.create(null); +} + /** * A key value store that is transferred from the application on the server side to the application * on the client side. @@ -79,15 +84,20 @@ export class TransferState { }); /** @internal */ - store: Record = {}; + store: Record = createDictionary(); - private onSerializeCallbacks: {[k: string]: () => unknown | undefined} = {}; + private onSerializeCallbacks: {[k: string]: () => unknown | undefined} = createDictionary(); /** * Get the value corresponding to a key. Return `defaultValue` if key is not found. */ get(key: StateKey, defaultValue: T): T { - return this.store[key] !== undefined ? (this.store[key] as T) : defaultValue; + if (!Object.hasOwn(this.store, key)) { + return defaultValue; + } + + const value = this.store[key]; + return value !== undefined ? (value as T) : defaultValue; } /** @@ -159,11 +169,14 @@ export function retrieveTransferredState( // Avoid using any here as it triggers lint errors in google3 (any is not allowed). // Decoding of `<` is done of the box by browsers and node.js, same behaviour as G3 // script_builders. - return JSON.parse(script.textContent) as {}; + return Object.assign( + createDictionary(), + JSON.parse(script.textContent) as {}, + ); } catch (e) { console.warn('Exception while restoring TransferState for app ' + appId, e); } } - return {}; + return createDictionary(); } diff --git a/packages/core/test/bundling/hydration/bundle.golden_symbols.json b/packages/core/test/bundling/hydration/bundle.golden_symbols.json index d0b6ed54c0d..e011ee34966 100644 --- a/packages/core/test/bundling/hydration/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hydration/bundle.golden_symbols.json @@ -435,6 +435,7 @@ "createContainerAnchorImpl", "createDeferBlockInjector", "createDehydratedBlockRegistry", + "createDictionary", "createDirectivesInstances", "createElementNode", "createElementRef", diff --git a/packages/core/test/transfer_state_spec.ts b/packages/core/test/transfer_state_spec.ts index adb0184834b..e5c78d8fe79 100644 --- a/packages/core/test/transfer_state_spec.ts +++ b/packages/core/test/transfer_state_spec.ts @@ -91,6 +91,31 @@ describe('TransferState', () => { expect(transferState.hasKey(TEST_KEY)).toBe(true); }); + it('does not read inherited properties', () => { + const transferState = TestBed.inject(TransferState); + const constructorKey = makeStateKey('constructor'); + const store = (transferState as unknown as {store: Record}).store; + Object.setPrototypeOf(store, {constructor: 'inherited'}); + + expect(transferState.get(constructorKey, 'default')).toBe('default'); + expect(transferState.hasKey(constructorKey)).toBeFalse(); + }); + + it('supports keys that name object prototype properties', () => { + const transferState = TestBed.inject(TransferState); + const prototypeKey = makeStateKey<{cached: boolean}>('__proto__'); + const value = {cached: true}; + + transferState.set(prototypeKey, value); + + expect(transferState.get(prototypeKey, {cached: false})).toBe(value); + expect(transferState.hasKey(prototypeKey)).toBeTrue(); + + transferState.remove(prototypeKey); + + expect(transferState.get(prototypeKey, null!)).toBeNull(); + }); + it("supports setting and accessing value '0' via get", () => { const transferState: TransferState = TestBed.inject(TransferState); transferState.set(TEST_KEY, 0); @@ -137,6 +162,16 @@ describe('TransferState', () => { expect(transferState.toJson()).toBe('{"test":20,"delayed":"changed"}'); }); + it('calls onSerialize callbacks whose keys name object prototype properties', () => { + const transferState = TestBed.inject(TransferState); + const prototypeKey = makeStateKey('__proto__'); + + transferState.onSerialize(prototypeKey, () => 'serialized'); + + expect(transferState.toJson()).toBe('{"__proto__":"serialized"}'); + expect(transferState.get(prototypeKey, null!)).toBe('serialized'); + }); + it('should provide an ability to detect whether the state is empty', () => { const transferState = TestBed.inject(TransferState); @@ -190,4 +225,14 @@ describe('TransferState', () => { expect(transferState.get(DELAYED_KEY, null)).toBe(relativeLink); expect(transferState.toJson()).toBe(encodedState); }); + + it('restores keys that name object prototype properties into a dictionary', () => { + addScriptTag(doc, APP_ID, '{"__proto__":{"cached":true},"constructor":"state"}'); + const transferState = TestBed.inject(TransferState); + + expect(transferState.get(makeStateKey<{cached: boolean}>('__proto__'), null)).toEqual({ + cached: true, + }); + expect(transferState.get(makeStateKey('constructor'), null)).toBe('state'); + }); });