/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/core/test/transfer_state_spec.ts
193 строки
7 KB
Matthieu Riegler
fix(core): harden TransferState restoration against DOM clobbering
04 июн 2026, 02:02
04 июн 2026, 02:02
6bde84f
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {APP_ID as APP_ID_TOKEN, PLATFORM_ID} from '../src/core'; import {TestBed} from '../testing'; import {DOCUMENT} from '../src/document'; import {makeStateKey, TransferState} from '../src/transfer_state'; function removeScriptTag(doc: Document, id: string) { let node = doc.getElementById(id); while (node) { node.remove(); node = doc.getElementById(id); } } function addScriptTag(doc: Document, appId: string, data: object | string) { const script = doc.createElement('script'); const id = appId + '-state'; script.id = id; script.setAttribute('type', 'application/json'); script.textContent = typeof data === 'string' ? data : JSON.stringify(data); // Remove any stale script tags. removeScriptTag(doc, id); doc.body.appendChild(script); } describe('TransferState', () => { const APP_ID = 'test-app'; let doc: Document; const TEST_KEY = makeStateKey<number>('test'); const BOOLEAN_KEY = makeStateKey<boolean>('boolean'); const DELAYED_KEY = makeStateKey<string>('delayed'); beforeEach(() => { TestBed.configureTestingModule({ providers: [ {provide: APP_ID_TOKEN, useValue: APP_ID}, {provide: PLATFORM_ID, useValue: 'browser'}, ], }); doc = TestBed.inject(DOCUMENT); }); afterEach(() => { removeScriptTag(doc, APP_ID + '-state'); }); it('is initialized from script tag', () => { addScriptTag(doc, APP_ID, {test: 10}); const transferState: TransferState = TestBed.inject(TransferState); expect(transferState.get(TEST_KEY, 0)).toBe(10); }); it('ignores non-script elements that clobber the transfer state id', () => { const id = APP_ID + '-state'; const clobberingNode = doc.createElement('div'); clobberingNode.id = id; clobberingNode.textContent = '{"test":999}'; doc.body.appendChild(clobberingNode); const script = doc.createElement('script'); script.id = id; script.setAttribute('type', 'application/json'); script.textContent = '{"test":10}'; doc.body.appendChild(script); const transferState: TransferState = TestBed.inject(TransferState); expect(transferState.get(TEST_KEY, 0)).toBe(0); }); it('is initialized to empty state if script tag not found', () => { const transferState: TransferState = TestBed.inject(TransferState); expect(transferState.get(TEST_KEY, 0)).toBe(0); }); it('supports adding new keys using set', () => { const transferState: TransferState = TestBed.inject(TransferState); transferState.set(TEST_KEY, 20); expect(transferState.get(TEST_KEY, 0)).toBe(20); expect(transferState.hasKey(TEST_KEY)).toBe(true); }); it("supports setting and accessing value '0' via get", () => { const transferState: TransferState = TestBed.inject(TransferState); transferState.set(TEST_KEY, 0); expect(transferState.get(TEST_KEY, 20)).toBe(0); expect(transferState.hasKey(TEST_KEY)).toBe(true); }); it("supports setting and accessing value 'false' via get", () => { const transferState: TransferState = TestBed.inject(TransferState); transferState.set(BOOLEAN_KEY, false); expect(transferState.get(BOOLEAN_KEY, true)).toBe(false); expect(transferState.hasKey(BOOLEAN_KEY)).toBe(true); }); it("supports setting and accessing value 'null' via get", () => { const transferState: TransferState = TestBed.inject(TransferState); transferState.set(TEST_KEY, null); expect(transferState.get(TEST_KEY, 20 as any)).toBe(null); expect(transferState.hasKey(TEST_KEY)).toBe(true); }); it('supports removing keys', () => { const transferState: TransferState = TestBed.inject(TransferState); transferState.set(TEST_KEY, 20); transferState.remove(TEST_KEY); expect(transferState.get(TEST_KEY, 0)).toBe(0); expect(transferState.hasKey(TEST_KEY)).toBe(false); }); it('supports serialization using toJson()', () => { const transferState: TransferState = TestBed.inject(TransferState); transferState.set(TEST_KEY, 20); expect(transferState.toJson()).toBe('{"test":20}'); }); it('calls onSerialize callbacks when calling toJson()', () => { const transferState: TransferState = TestBed.inject(TransferState); transferState.set(TEST_KEY, 20); let value = 'initial'; transferState.onSerialize(DELAYED_KEY, () => value); value = 'changed'; expect(transferState.toJson()).toBe('{"test":20,"delayed":"changed"}'); }); it('should provide an ability to detect whether the state is empty', () => { const transferState = TestBed.inject(TransferState); // The state is empty initially. expect(transferState.isEmpty).toBeTrue(); transferState.set(TEST_KEY, 20); expect(transferState.isEmpty).toBeFalse(); transferState.remove(TEST_KEY); expect(transferState.isEmpty).toBeTrue(); }); it('should encode `<` to avoid breaking out of <script> tag in serialized output', () => { const transferState = TestBed.inject(TransferState); // The state is empty initially. expect(transferState.isEmpty).toBeTrue(); transferState.set(DELAYED_KEY, '</script><script>alert(\'Hello&\' + "World");'); expect(transferState.toJson()).toBe( `{"delayed":"\\u003C\\u002Fscript>\\u003Cscript>alert('Hello&' + \\"World\\");"}`, ); }); it('should encode `/` to avoid crawler indexing of inline JSON', () => { const transferState = TestBed.inject(TransferState); transferState.set(DELAYED_KEY, '/foo/bar'); expect(transferState.toJson()).toBe(`{"delayed":"\\u002Ffoo\\u002Fbar"}`); }); it('should decode `\\u003C` (<) and `\\u002F` (/) when restoring stating', () => { const encodedState = `{"delayed":"\\u003C\\u002Fscript>\\u003Cscript>alert('Hello&' + \\"World\\");"}`; addScriptTag(doc, APP_ID, encodedState); const transferState = TestBed.inject(TransferState); expect(transferState.toJson()).toBe(encodedState); expect(transferState.get(DELAYED_KEY, null)).toBe( '</script><script>alert(\'Hello&\' + "World");', ); }); it('should properly encode and decode relative links in JSON', () => { const relativeLink = '/about/us?query=1'; const encodedState = `{"delayed":"\\u002Fabout\\u002Fus?query=1"}`; // Ensure restoring from the encoded state correctly decodes the relative link addScriptTag(doc, APP_ID, encodedState); const transferState = TestBed.inject(TransferState); expect(transferState.get(DELAYED_KEY, null)).toBe(relativeLink); expect(transferState.toJson()).toBe(encodedState); }); });