Angular Client Hydration DOM Clobbering & Response-Cache Poisoning
Description
Angular's SSR hydration state lookup via document.getElementById('ng-state') allows DOM Clobbering, enabling HTTP cache poisoning leading to XSS.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Angular's SSR hydration state lookup via `document.getElementById('ng-state')` allows DOM Clobbering, enabling HTTP cache poisoning leading to XSS.
Vulnerability
Angular's SSR hydration uses document.getElementById('ng-state') to retrieve a serialized ` tag containing runtime state, including cached HttpClient responses [1], [2]. The fixed id attribute ng-state makes the lookup susceptible to DOM Clobbering if an application renders untrusted user or CMS content into an element's id attribute before the genuine script tag is parsed [1], [2]. Affected versions include all Angular releases using provideClientHydration() prior to the fix applied in commit 6bde84f` [3].
Exploitation
An attacker with the ability to inject HTML (e.g., via a CMS field or a template binding like [id]=userInput) can place a non-script element such as ` in the DOM before the legitimate ng-state script tag is parsed [1], [2]. When Angular client-side hydration calls document.getElementById('ng-state')`, the browser returns the attacker's clobbered element instead, and Angular attempts to parse its text content as JSON [1], [2].
Impact
The attacker can inject a crafted JSON payload into Angular's TransferState cache, poisoning the HTTP transfer cache [1], [2]. This causes HttpClient to return a forged response instead of making a real API request [1], [2]. Depending on how the application processes the poisoned data, this can lead to DOM-based Cross-Site Scripting (XSS) if unsafe bindings are used, or privilege escalation by spoofing backend responses [1], [2].
Mitigation
The Angular team committed fix 6bde84f which, when reading the transfer state, rejects non-script elements with the clobbered id by iteratively removing all matching elements until only the genuine ` tag is found [3], [4]. The fix is available in the Angular repository and the advisory recommends upgrading to the patched version [4]. If patching is not immediately possible, avoid rendering untrusted input into element id attributes, and validate that no user‑controlled content can create elements with id="ng-state"` [1], [2].
- CVE-2026-54267 - GitHub Advisory Database
- Angular Client Hydration DOM Clobbering & Response-Cache Poisoning
- fix(core): harden TransferState restoration against DOM clobbering · angular/angular@6bde84f
- fix(core): harden TransferState restoration against DOM clobbering by JeanMeche · Pull Request #69064 · angular/angular
AI Insight generated on Jun 15, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
16bde84fa8e6afix(core): harden TransferState restoration against DOM clobbering
2 files changed · +24 −2
packages/core/src/transfer_state.ts+1 −1 modified@@ -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
packages/core/test/transfer_state_spec.ts+23 −1 modified@@ -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);
Vulnerability mechanics
Root cause
"Missing element-type validation in `retrieveTransferredState` allows any DOM element with a clobbered `id` to be accepted as the transfer state container."
Attack vector
An attacker injects a non-script element (e.g., `<div id="ng-state">`) into the DOM before the genuine `<script id="ng-state">` tag is parsed. When Angular's hydration code calls `document.getElementById('ng-state')`, the browser returns the attacker's element. Angular then parses its `textContent` as JSON, poisoning the `TransferState` cache. This can lead to DOM-based XSS, privilege escalation, or UI hijacking if the poisoned data is rendered unsafely [CWE-79].
Affected code
The vulnerability resides in `packages/core/src/transfer_state.ts` in the `retrieveTransferredState` function. The function used `doc.getElementById(appId + '-state')` and then accessed `.textContent` without verifying that the returned element is a `<script>` tag. The patch adds a `tagName === 'SCRIPT'` guard to reject non-script elements that clobber the expected ID.
What the fix does
The patch adds a `tagName === 'SCRIPT'` check in `retrieveTransferredState` so that only `<script>` elements are accepted as the transfer state container. This prevents any attacker-controlled element (e.g., `<div>`, `<a>`) with a clobbered `id` from being parsed as hydration state. The test also verifies that when a clobbering `<div>` appears before the genuine `<script>`, the transfer state remains empty.
Preconditions
- configThe application must use Angular's `provideClientHydration()` with SSR.
- inputAn attacker must be able to inject an HTML element with `id="ng-state"` (or the custom app ID suffix) into the DOM before the genuine script tag is parsed.
Generated on Jun 15, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.