{"id":"GHSA-ffj9-42qx-f38h","kind":"vulnerability","aliases":["GHSA-ffj9-42qx-f38h"],"related":[],"title":"Cross-Site Scripting (XSS) via AngularJS Double Compilation in @angular/upgrade's downgradeComponent()","description":"A Cross-Site Scripting (XSS) vulnerability was identified in `@angular/upgrade` when using `downgradeComponent()` in hybrid Angular / AngularJS applications.\r\n\r\n### Technical Description\r\n\r\nWhen 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.\r\n\r\nPreviously, `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.\r\n\r\nAlthough 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.\r\n\r\nWhen 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.\r\n\r\n### Impact\r\n\r\nSuccessful 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).\r\n\r\n* **Pure Angular Applications Unaffected:** Standard Angular applications that do not run a hybrid AngularJS setup via `@angular/upgrade` (`downgradeComponent()`) are **not** vulnerable.\r\n\r\n### Attack Preconditions\r\n\r\nAn application is affected if the following conditions are met:\r\n\r\n* **Hybrid Application:** The application uses `@angular/upgrade` (`downgradeComponent()`) to embed Angular components inside AngularJS templates.\r\n* **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.\r\n* **User-Controlled Data:** The content written or transcluded into the downgraded host element includes untrusted user input.\r\n\r\n#### Exploit Payload Example\r\n\r\n```html\r\n<user-badge ng-bind=\"$ctrl.user.name\"></user-badge>\r\n```\r\n\r\nIf `$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.\r\n\r\n### Patches\r\n\r\nThe 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()`.\r\n\r\n* `22.2.4`\r\n* `21.2.26`\r\n* `20.3.35`\r\n\r\n### Workarounds\r\n\r\nIf 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.\r\n\r\n  * 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>`).\r\n  * 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>`).\r\n\r\n### References\r\n\r\n* **Issue:** https://github.com/angular/angular/issues/71441\r\n* **Fix PR:** https://github.com/angular/angular/pull/71442\r\n* **Backport 22.2.x:** https://github.com/angular/angular/pull/71614\r\n* **Backport 21.2.x:** https://github.com/angular/angular/pull/71610\r\n* **Backport 20.3.x:** https://github.com/angular/angular/pull/71611","summary":"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.","severity":"medium","cvss":6.3,"epss":null,"exploited":false,"exploited_at":null,"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"],"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.\n- 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.\n- Pure Angular apps without `@angular/upgrade` are not affected.","fix":"- `@angular/upgrade` `22.2.4` (22.x line)\n- `@angular/upgrade` `21.2.26` (21.x line)\n- `@angular/upgrade` `20.3.35` (20.x line)\n- Versions `19.2.25` and below: no fix, end of support; the sources say it will not be patched.","status":"open","published_at":"2026-11-09T08:27:11+00:00","first_seen_at":"2026-11-09T22:53:17+00:00","triaged_at":"2026-11-09T22:54:47+00:00","updated_at":"2026-11-09T22:54:47+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-11-09T22:54:47Z","model":"claude-haiku-5-5","output":{"fix":"- `@angular/upgrade` `22.2.4` (22.x line)\n- `@angular/upgrade` `21.2.26` (21.x line)\n- `@angular/upgrade` `20.3.35` (20.x line)\n- Versions `19.2.25` and below: no fix, end of support; the sources say it will not be patched.","reach":"internet","title":"Angular upgrade: AngularJS double compilation enables cross-site scripting in hybrid apps","impact":"high","summary":"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.","audience":"developers","category":"package","nicknames":[],"who_cares":["angular","angularjs","hybrid-apps","web-frontend"],"confidence":0.85,"fix_status":"available","needs_auth":"unknown","worst_case":"attacker runs arbitrary JavaScript in victim's browser session","analyst_note":"","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.\n- 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.\n- Pure Angular apps without `@angular/upgrade` are not affected.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["pure-angular","no-downgradeComponent","no-host-directives","static-content-only"],"who_is_affected":"Only hybrid applications that use @angular/upgrade's downgradeComponent() to embed Angular components inside AngularJS templates are exposed. The risk requires a ng-bind, ng-bind-html or ng-transclude directive on a downgraded component's host element and untrusted user data rendered there. Pure Angular apps are not affected.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.00103,"input_tokens":6396,"output_tokens":780,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-79':31B '0':202C,206C '1':128C '20.3.35':577C '21.2.26':576C '22.2.4':575C '42qx':17A,22A 'affect':386C 'alert':487C 'allow':327C 'alreadi':244C 'already-link':243C 'although':186C 'angular':28B,41B,110C,117C,169C,361C,365C,402C 'angular/upgrade':11A,24A,25A,26A,27A,45B,104C,376C,398C 'angularj':7A,29B,43B,55B,111C,127C,147C,199C,207C,288C,321C,373C,405C,413C,482C 'app':34B,44B 'applic':91B,112C,353C,362C,366C,384C,394C,396C 'arbitrari':82B,340C 'asynchron':260C 'attack':75B,329C,381C 'base':296C 'bind':66B,219C,222C,302C,309C,428C,431C,593C,596C,612C 'bootstrap':262C 'bound':239C 'browser':88B,347C 'cannot':581C 'captur':176C,520C 'child':136C,157C,181C,530C 'class':295C,299C 'class-bas':294C 'clone':522C 'co':60B,233C,408C,491C 'co-loc':59B,232C,407C,490C 'compil':9A,48B,148C,154C,286C,316C,451C,510C,554C,574C 'compilecont':151C,255C,280C 'compon':118C,170C,403C,443C,606C 'condit':390C 'consequ':203C 'constructor.constructor':486C 'contain':480C 'content':52B,140C,284C,421C,455C,461C,508C 'context':350C 'control':77B,331C,458C,536C 'creat':173C 'cross':2A,96C,357C 'cross-sit':1A,95C,356C 'ctrl.user.name':479C 'cwe':30B 'data':70B,79B,240C,314C,332C,459C 'default':198C 'descript':114C 'direct':62B,188C,200C,208C,235C,275C,291C,297C,411C,414C,439C,493C,534C,541C,571C,602C 'document.domain':488C 'dom':137C,247C,420C,556C 'doubl':8A 'downgrad':120C,168C,251C,274C,336C,442C,467C,525C,605C,615C 'downgradecompon':13A,47B,107C,122C,191C,377C,399C,450C,501C 'downgradecomponentadapter.compilecontents':131C 'downgrademodul':261C 'dynam':49B,283C,558C 'e.g':485C 'element':51B,134C,162C,214C,253C,337C,418C,446C,453C,469C,506C,526C,609C,617C 'emb':401C 'empti':568C 'evalu':511C 'exampl':476C 'execut':229C,270C,318C,339C,545C 'exploit':326C,474C 'express':323C,484C,513C 'f38h':18A,23A 'ffj9':16A,21A 'first':73B 'follow':389C 'frontend':38B 'ghsa':15A,20A 'ghsa-ffj9-42qx-f38h':14A,19A 'host':50B,161C,213C,252C,410C,445C,468C,505C,608C,616C 'html':223C,310C,432C,477C,597C 'hybrid':33B,40B,109C,372C,393C 'hybrid-app':32B 'identifi':102C 'immedi':582C 'impact':324C 'includ':293C,470C 'input':473C 'insert':242C,559C 'insid':125C,404C,532C 'instanc':171C 'instead':610C 'interpol':289C 'invok':573C 'issu':516C 'javascript':83B,341C 'like':63B,298C 'link':185C,245C,269C,279C,423C,499C,561C 'live':156C 'locat':61B,234C,409C,492C 'met':392C 'ng':65B,218C,221C,226C,301C,308C,427C,430C,435C,592C,595C,600C 'ng-bind':64B,217C,300C,426C,591C 'ng-bind-html':220C,307C,429C,594C 'ng-transclud':225C,434C,599C 'ngtransclud':265C 'node':138C,158C,182C,248C,531C,552C,557C 'origin':93B,178C,355C,565C 'packag':35B 'pass':143C,281C,503C,548C 'patch':514C,586C 'payload':475C 'phase':537C 'place':209C,438C,590C 'popul':416C 'post':268C,278C 'post-link':267C,277C 'postlink':544C 'precondit':382C 'prelink':542C 'prepar':132C 'present':311C 'previous':150C 'prior':183C,271C,538C 'prioriti':201C,205C 'project':141C,570C 'pure':360C 'rather':174C 'read':152C 'regist':189C 'render':333C 'resolv':518C 'run':81B,256C,370C,448C 'sanit':305C 'script':4A,98C,359C 'servic':149C 'setup':374C 'site':3A,97C,358C 'specifi':192C 'standard':364C 'static':179C,528C,550C 'still':228C 'subsequ':502C 'success':325C 'surviv':304C 'syntax':292C 'technic':113C 'templat':56B,130C,180C,322C,406C,483C,529C,551C,566C 'termin':193C 'this.element.contents':163C 'time':166C,562C 'transclud':227C,246C,436C,464C,601C 'true':194C 'unaffect':363C 'untrust':471C 'upgrad':583C 'use':106C,124C,196C,397C 'user':69B,238C,345C,457C,472C 'user-bound':237C 'user-control':456C 'valu':496C 'version':587C 'via':6A,121C,375C 'victim':86B,344C 'vulner':100C,380C 'web':37B 'web-frontend':36B 'within':334C,348C 'without':572C 'workaround':578C 'write':236C,494C 'written':68B,462C 'x':129C 'xss':5A,99C","nicknames":[],"mitigations":[],"who_affected":"Only hybrid applications that use @angular/upgrade's downgradeComponent() to embed Angular components inside AngularJS templates are exposed. The risk requires a ng-bind, ng-bind-html or ng-transclude directive on a downgraded component's host element and untrusted user data rendered there. Pure Angular apps are not affected.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["upgrade"],"ptokens":["angular","upgrade"],"labels":["angular","angularjs","cwe79","hybridapps","medium","npm","package","webfrontend"],"campaign_type":null,"search_a":"'42qx':17A,22A 'angular/upgrade':11A,24A,25A,26A,27A 'angularj':7A 'compil':9A 'cross':2A 'cross-sit':1A 'doubl':8A 'downgradecompon':13A 'f38h':18A,23A 'ffj9':16A,21A 'ghsa':15A,20A 'ghsa-ffj9-42qx-f38h':14A,19A 'script':4A 'site':3A 'via':6A 'xss':5A","search_b":"'-79':31B 'angular':28B,41B 'angular/upgrade':45B 'angularj':29B,43B,55B 'app':34B,44B 'applic':91B 'arbitrari':82B 'attack':75B 'bind':66B 'browser':88B 'co':60B 'co-loc':59B 'compil':48B 'content':52B 'control':77B 'cwe':30B 'data':70B,79B 'direct':62B 'downgradecompon':47B 'dynam':49B 'element':51B 'first':73B 'frontend':38B 'host':50B 'hybrid':33B,40B 'hybrid-app':32B 'javascript':83B 'like':63B 'locat':61B 'ng':65B 'ng-bind':64B 'origin':93B 'packag':35B 'run':81B 'templat':56B 'user':69B 'victim':86B 'web':37B 'web-frontend':36B 'written':68B","search_c":"'0':202C,206C '1':128C '20.3.35':577C '21.2.26':576C '22.2.4':575C 'affect':386C 'alert':487C 'allow':327C 'alreadi':244C 'already-link':243C 'although':186C 'angular':110C,117C,169C,361C,365C,402C 'angular/upgrade':104C,376C,398C 'angularj':111C,127C,147C,199C,207C,288C,321C,373C,405C,413C,482C 'applic':112C,353C,362C,366C,384C,394C,396C 'arbitrari':340C 'asynchron':260C 'attack':329C,381C 'base':296C 'bind':219C,222C,302C,309C,428C,431C,593C,596C,612C 'bootstrap':262C 'bound':239C 'browser':347C 'cannot':581C 'captur':176C,520C 'child':136C,157C,181C,530C 'class':295C,299C 'class-bas':294C 'clone':522C 'co':233C,408C,491C 'co-loc':232C,407C,490C 'compil':148C,154C,286C,316C,451C,510C,554C,574C 'compilecont':151C,255C,280C 'compon':118C,170C,403C,443C,606C 'condit':390C 'consequ':203C 'constructor.constructor':486C 'contain':480C 'content':140C,284C,421C,455C,461C,508C 'context':350C 'control':331C,458C,536C 'creat':173C 'cross':96C,357C 'cross-sit':95C,356C 'ctrl.user.name':479C 'data':240C,314C,332C,459C 'default':198C 'descript':114C 'direct':188C,200C,208C,235C,275C,291C,297C,411C,414C,439C,493C,534C,541C,571C,602C 'document.domain':488C 'dom':137C,247C,420C,556C 'downgrad':120C,168C,251C,274C,336C,442C,467C,525C,605C,615C 'downgradecompon':107C,122C,191C,377C,399C,450C,501C 'downgradecomponentadapter.compilecontents':131C 'downgrademodul':261C 'dynam':283C,558C 'e.g':485C 'element':134C,162C,214C,253C,337C,418C,446C,453C,469C,506C,526C,609C,617C 'emb':401C 'empti':568C 'evalu':511C 'exampl':476C 'execut':229C,270C,318C,339C,545C 'exploit':326C,474C 'express':323C,484C,513C 'follow':389C 'host':161C,213C,252C,410C,445C,468C,505C,608C,616C 'html':223C,310C,432C,477C,597C 'hybrid':109C,372C,393C 'identifi':102C 'immedi':582C 'impact':324C 'includ':293C,470C 'input':473C 'insert':242C,559C 'insid':125C,404C,532C 'instanc':171C 'instead':610C 'interpol':289C 'invok':573C 'issu':516C 'javascript':341C 'like':298C 'link':185C,245C,269C,279C,423C,499C,561C 'live':156C 'locat':234C,409C,492C 'met':392C 'ng':218C,221C,226C,301C,308C,427C,430C,435C,592C,595C,600C 'ng-bind':217C,300C,426C,591C 'ng-bind-html':220C,307C,429C,594C 'ng-transclud':225C,434C,599C 'ngtransclud':265C 'node':138C,158C,182C,248C,531C,552C,557C 'origin':178C,355C,565C 'pass':143C,281C,503C,548C 'patch':514C,586C 'payload':475C 'phase':537C 'place':209C,438C,590C 'popul':416C 'post':268C,278C 'post-link':267C,277C 'postlink':544C 'precondit':382C 'prelink':542C 'prepar':132C 'present':311C 'previous':150C 'prior':183C,271C,538C 'prioriti':201C,205C 'project':141C,570C 'pure':360C 'rather':174C 'read':152C 'regist':189C 'render':333C 'resolv':518C 'run':256C,370C,448C 'sanit':305C 'script':98C,359C 'servic':149C 'setup':374C 'site':97C,358C 'specifi':192C 'standard':364C 'static':179C,528C,550C 'still':228C 'subsequ':502C 'success':325C 'surviv':304C 'syntax':292C 'technic':113C 'templat':130C,180C,322C,406C,483C,529C,551C,566C 'termin':193C 'this.element.contents':163C 'time':166C,562C 'transclud':227C,246C,436C,464C,601C 'true':194C 'unaffect':363C 'untrust':471C 'upgrad':583C 'use':106C,124C,196C,397C 'user':238C,345C,457C,472C 'user-bound':237C 'user-control':456C 'valu':496C 'version':587C 'via':121C,375C 'victim':344C 'vulner':100C,380C 'within':334C,348C 'without':572C 'workaround':578C 'write':236C,494C 'written':462C 'x':129C 'xss':99C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-09T08:27:11+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-ffj9-42qx-f38h","url":"https://github.com/angular/angular/security/advisories/GHSA-ffj9-42qx-f38h","severity":"medium","published_at":"2026-11-09T08:27:11+00:00","modified_at":"2026-11-09T08:27:11+00:00","first_seen_at":"2026-11-09T22:53:17+00:00","fetched_at":"2026-11-09T22:53:17+00:00","note":null}],"documents":[{"id":110453,"url":"https://github.com/angular/angular/security/advisories/GHSA-ffj9-42qx-f38h","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}