VYPR
\n````\n\nDuring client bootstrap, Angular recovers this state by looking up the element via `document.getElementById('ng-state')` and parsing its text content.\n\nBecause the DOM element lookup for the state container is predictable and relies solely on the ID selector (`ng-state`), it is susceptible to **DOM Clobbering**.\n\nIf the application binds untrusted user input or CMS content to element properties such as `id` (e.g., `
` or ``) *before* the genuine `
High severity8.6GHSA Advisory· Published Jun 15, 2026

Angular Client Hydration DOM Clobbering & Response-Cache Poisoning

CVE-2026-54267

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].

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

1

Patches

1
6bde84fa8e6a

fix(core): harden TransferState restoration against DOM clobbering

https://github.com/angular/angularMatthieu RieglerJun 1, 2026via body-scan-shorthand
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

4

News mentions

0

No linked articles in our index yet.