---
id: "GHSA-ffj9-42qx-f38h"
page: "https://infrastack.ai/live/GHSA-ffj9-42qx-f38h"
kind: "vulnerability"
aliases: ["GHSA-ffj9-42qx-f38h"]
title: "Cross-Site Scripting (XSS) via AngularJS Double Compilation in @angular/upgrade's downgradeComponent()"
severity: "medium"
cvss: 6.3
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "@angular/upgrade", "fixed": ["22.2.4"], "affected": ">= 22.0.0, < 22.2.4", "ecosystem": "npm"}, {"name": "@angular/upgrade", "fixed": ["21.2.26"], "affected": ">= 21.0.0, < 21.2.26", "ecosystem": "npm"}, {"name": "@angular/upgrade", "fixed": ["20.3.35"], "affected": ">= 20.0.0, < 20.3.35", "ecosystem": "npm"}, {"name": "@angular/upgrade", "fixed": ["End of Support / LTS expired; will not be patched"], "affected": "<= 19.2.25", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["angular", "angularjs", "cwe-79", "hybrid-apps", "package", "web-frontend"]
not_affected_if: ["no-downgradecomponent", "no-host-directives", "pure-angular", "static-content-only"]
published_at: "2026-11-09T08:27:11+00:00"
first_seen_at: "2026-11-09T22:53:17+00:00"
updated_at: "2026-11-09T22:54:47+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/angular/angular/security/advisories/GHSA-ffj9-42qx-f38h", "first_seen_at": "2026-11-09T22:53:17+00:00"}]
exploit_sources: []
---

# Cross-Site Scripting (XSS) via AngularJS Double Compilation in @angular/upgrade's downgradeComponent()

Link for people: https://infrastack.ai/live/GHSA-ffj9-42qx-f38h (this .md is for agents)

In hybrid Angular and AngularJS apps, @angular/upgrade's downgradeComponent() compiles dynamic host element content as an AngularJS template when a co-located directive like ng-bind has written user data into it first. An attacker who controls that data can run arbitrary JavaScript in the victim's browser under the application's origin.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:@angular/upgrade affected >= 22.0.0, < 22.2.4 fixed 22.2.4
- npm:@angular/upgrade affected >= 21.0.0, < 21.2.26 fixed 21.2.26
- npm:@angular/upgrade affected >= 20.0.0, < 20.3.35 fixed 20.3.35
- npm:@angular/upgrade affected <= 19.2.25 fixed End of Support / LTS expired; will not be patched

## How to check

- Search templates for `downgradeComponent` usage and for `ng-bind`, `ng-bind-html`, or `ng-transclude` on the same element as a downgraded component's host tag.
- Check the installed version with `npm ls @angular/upgrade`; versions below `20.3.35`, `21.2.26` or `22.2.4` in their branch are affected.
- Pure Angular apps without `@angular/upgrade` are not affected.

## Fix

- `@angular/upgrade` `22.2.4` (22.x line)
- `@angular/upgrade` `21.2.26` (21.x line)
- `@angular/upgrade` `20.3.35` (20.x line)
- Versions `19.2.25` and below: no fix, end of support; the sources say it will not be patched.

## Timeline

- 2026-11-09T22:53:17+00:00 ghsa-repo https://github.com/angular/angular/security/advisories/GHSA-ffj9-42qx-f38h

## Documents

- [advisory] Maintainer's advisory: github.com https://github.com/angular/angular/security/advisories/GHSA-ffj9-42qx-f38h

## From the sources

A Cross-Site Scripting (XSS) vulnerability was identified in `@angular/upgrade` when using `downgradeComponent()` in hybrid Angular / AngularJS applications.

### Technical Description

When an Angular component is downgraded via `downgradeComponent()` for use inside an AngularJS (1.x) template, `DowngradeComponentAdapter.compileContents()` prepares the element's child DOM nodes for content projection (`<ng-content>`) by passing them to the AngularJS `$compile()` service.

Previously, `compileContents()` read and compiled the live child nodes of the host element (`this.element.contents()`) at the time the downgraded Angular component instance was created, rather than capturing the original static template child nodes prior to linking.

Although the directive registered by `downgradeComponent()` specifies `terminal: true`, it uses the default AngularJS directive priority (`0`). Consequently, other priority-`0` AngularJS directives placed on the same host element (such as `ng-bind`, `ng-bind-html`, or `ng-transclude`) still execute.

When a co-located directive writes user-bound data or inserts already-linked transcluded DOM nodes into the downgraded host element *before* `compileContents()` runs (such as during asynchronous `downgradeModule()` bootstrapping or when `ngTransclude`'s post-link executes prior to the downgraded directive's post-link), `compileContents()` passes that dynamic content to `$compile()`. Any AngularJS interpolation (`{{ ... }}`) or directive syntax (including class-based directives like `class="ng-bind: ..."` that survive `$sanitize` in `ng-bind-html`) present in the data is compiled and executed as an AngularJS template expression.

### Impact

Successful exploitation allows an attacker who controls data rendered within the downgraded element to execute arbitrary JavaScript in the victim user's browser within the context of the application's origin (Cross-Site Scripting).

* **Pure Angular Applications Unaffected:** Standard Angular applications that do not run a hybrid AngularJS setup via `@angular/upgrade` (`downgradeComponent()`) are **not** vulnerable.

### Attack Preconditions

An application is affected if the following conditions are met:

* **Hybrid Application:** The application uses `@angular/upgrade` (`downgradeComponent()`) to embed Angular components inside AngularJS templates.
* **Co-located Host Directive:** An AngularJS directive that populates the element's DOM contents during linking (such as `ng-bind`, `ng-bind-html`, or `ng-transclude`) is placed directly on a downgraded component's host element and runs before `downgradeComponent()` compiles the element's contents.
* **User-Controlled Data:** The content written or transcluded into the downgraded host element includes untrusted user input.

#### Exploit Payload Example

```html
<user-badge ng-bind="$ctrl.user.name"></user-badge>
```

If `$ctrl.user.name` contains an AngularJS template expression (e.g., `{{constructor.constructor('alert(document.domain)')()}}`), the co-located directive writes the value into `<user-badge>` during linking, and `downgradeComponent()` subsequently passes the host element's contents to `$compile()`, evaluating the expression.

### Patches

The issue is resolved by capturing a clone of the downgraded element's static template child nodes inside the directive's `controller` phase (prior to any directive `preLink` or `postLink` execution) and only passing those static template nodes to `$compile()`. Any DOM nodes dynamically inserted at link time when the original template was empty are projected directly without invoking `$compile()`.

* `22.2.4`
* `21.2.26`
* `20.3.35`

### Workarounds

If you cannot immediately upgrade to a patched version, do not place `ng-bind`, `ng-bind-html`, or `ng-transclude` directly on a downgraded component's host element.

  * Instead of binding on the downgraded host element (`<user-badge ng-bind="$ctrl.user.name"></user-badge>`), move the binding onto an inner child element (`<user-badge><span ng-bind="$ctrl.user.name"></span></user-badge>`) or pass the value via an Angular `@Input()` property binding (`<user-badge [name]="$ctrl.user.name"></user-badge>`).
  * Instead of placing `ng-transclude` directly on the downgraded host element (`<app-card ng-transclude></app-card>`), place `ng-transclude` on a wrapper child element inside the downgraded component (`<app-card><div ng-transclude></div></app-card>`).

### References

* **Issue:** https://github.com/angular/angular/issues/71441
* **Fix PR:** https://github.com/angular/angular/pull/71442
* **Backport 22.2.x:** https://github.com/angular/angular/pull/71614
* **Backport 21.2.x:** https://github.com/angular/angular/pull/71610
* **Backport 20.3.x:** https://github.com/angular/angular/pull/71611

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
