fix(core): harden TransferState restoration against DOM clobbering

Reject non-script elements when reading the SSR transfer state payload by id.
This prevents attacker-controlled elements with a clobbered id from spoofing
hydration state.
This commit is contained in:
Matthieu Riegler
2026-06-01 22:09:41 +02:00
committed by Andrew Scott
parent 7896e74222
commit 6bde84fa8e
2 changed files with 24 additions and 2 deletions
+1 -1
View File
@@ -154,7 +154,7 @@ export function retrieveTransferredState(
// Locate the script tag with the JSON data transferred from the server.
// The id of the script tag is set to the Angular appId + 'state'.
const script = doc.getElementById(appId + '-state');
if (script?.textContent) {
if (script?.tagName === 'SCRIPT' && script.textContent) {
try {
// 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
+23 -1
View File
@@ -13,7 +13,11 @@ import {DOCUMENT} from '../src/document';
import {makeStateKey, TransferState} from '../src/transfer_state';
function removeScriptTag(doc: Document, id: string) {
doc.getElementById(id)?.remove();
let node = doc.getElementById(id);
while (node) {
node.remove();
node = doc.getElementById(id);
}
}
function addScriptTag(doc: Document, appId: string, data: object | string) {
@@ -57,6 +61,24 @@ describe('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);