CVE-2026-32635
Description
Angular is a development platform for building mobile and desktop web applications using TypeScript/JavaScript and other languages. Prior to 22.0.0-next.3, 21.2.4, 20.3.18, and 19.2.20, a Cross-Site Scripting (XSS) vulnerability has been identified in the Angular runtime and compiler. It occurs when the application uses a security-sensitive attribute (for example href on an anchor tag) together with Angular's ability to internationalize attributes. Enabling internationalization for the sensitive attribute by adding i18n-<attribute> name bypasses Angular's built-in sanitization mechanism, which when combined with a data binding to untrusted user-generated data can allow an attacker to inject a malicious script. This vulnerability is fixed in 22.0.0-next.3, 21.2.4, 20.3.18, and 19.2.20.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@angular/corenpm | >= 22.0.0-next.0, < 22.0.0-next.3 | 22.0.0-next.3 |
@angular/corenpm | >= 21.0.0-next.0, < 21.2.4 | 21.2.4 |
@angular/corenpm | >= 20.0.0-next.0.0.0, < 20.3.18 | 20.3.18 |
@angular/corenpm | >= 19.0.0-next.0, < 19.2.20 | 19.2.20 |
@angular/corenpm | >= 17.0.0-next.0, <= 18.2.14 | — |
@angular/compilernpm | >= 22.0.0-next.0, < 22.0.0-next.3 | 22.0.0-next.3 |
@angular/compilernpm | >= 21.0.0-next.0, < 21.2.4 | 21.2.4 |
@angular/compilernpm | >= 20.0.0-next.0.0.0, < 20.3.18 | 20.3.18 |
@angular/compilernpm | >= 19.0.0-next.0, < 19.2.20 | 19.2.20 |
@angular/compilernpm | >= 17.0.0-next.0, <= 18.2.14 | — |
Affected products
3cpe:2.3:a:angular:angular_cli:*:*:*:*:*:*:*:*+ 2 more
- cpe:2.3:a:angular:angular_cli:*:*:*:*:*:*:*:*range: >=17.0.0,<19.2.0
- cpe:2.3:a:angular:angular_cli:22.0.0:next0:*:*:*:*:*:*
- cpe:2.3:a:angular:angular_cli:22.0.0:next1:*:*:*:*:*:*
Patches
478dea55351fbfix(compiler): disallow translations of iframe src
2 files changed · +3 −1
packages/compiler/src/schema/trusted_types_sinks.ts+2 −1 modified@@ -11,7 +11,7 @@ * tags use '*'. * * Extracted from, and should be kept in sync with - * https://w3c.github.io/webappsec-trusted-types/dist/spec/#integrations + * https://www.w3.org/TR/trusted-types/#integrations */ const TRUSTED_TYPES_SINKS = new Set<string>([ // NOTE: All strings in this set *must* be lowercase! @@ -25,6 +25,7 @@ const TRUSTED_TYPES_SINKS = new Set<string>([ // TrustedScriptURL 'embed|src', + 'iframe|src', 'object|codebase', 'object|data', ]);
packages/compiler/test/schema/trusted_types_sinks_spec.ts+1 −0 modified@@ -13,6 +13,7 @@ describe('isTrustedTypesSink', () => { expect(isTrustedTypesSink('iframe', 'srcdoc')).toBeTrue(); expect(isTrustedTypesSink('p', 'innerHTML')).toBeTrue(); expect(isTrustedTypesSink('embed', 'src')).toBeTrue(); + expect(isTrustedTypesSink('iframe', 'src')).toBeTrue(); expect(isTrustedTypesSink('a', 'href')).toBeFalse(); expect(isTrustedTypesSink('base', 'href')).toBeFalse(); expect(isTrustedTypesSink('div', 'style')).toBeFalse();
ed2d324f9cc1fix(compiler): disallow translations of iframe src
2 files changed · +3 −1
packages/compiler/src/schema/trusted_types_sinks.ts+2 −1 modified@@ -11,7 +11,7 @@ * tags use '*'. * * Extracted from, and should be kept in sync with - * https://w3c.github.io/webappsec-trusted-types/dist/spec/#integrations + * https://www.w3.org/TR/trusted-types/#integrations */ const TRUSTED_TYPES_SINKS = new Set<string>([ // NOTE: All strings in this set *must* be lowercase! @@ -25,6 +25,7 @@ const TRUSTED_TYPES_SINKS = new Set<string>([ // TrustedScriptURL 'embed|src', + 'iframe|src', 'object|codebase', 'object|data', ]);
packages/compiler/test/schema/trusted_types_sinks_spec.ts+1 −0 modified@@ -13,6 +13,7 @@ describe('isTrustedTypesSink', () => { expect(isTrustedTypesSink('iframe', 'srcdoc')).toBeTrue(); expect(isTrustedTypesSink('p', 'innerHTML')).toBeTrue(); expect(isTrustedTypesSink('embed', 'src')).toBeTrue(); + expect(isTrustedTypesSink('iframe', 'src')).toBeTrue(); expect(isTrustedTypesSink('a', 'href')).toBeFalse(); expect(isTrustedTypesSink('base', 'href')).toBeFalse(); expect(isTrustedTypesSink('div', 'style')).toBeFalse();
8630319f74c9fix(core): sanitize translated attribute bindings with interpolations
2 files changed · +74 −13
packages/core/src/render3/i18n/i18n_parse.ts+9 −13 modified@@ -388,7 +388,7 @@ export function i18nAttributesFirstPass(tView: TView, index: number, values: str previousElementIndex, attrName, countBindings(updateOpCodes), - null, + URI_ATTRS[attrName.toLowerCase()] ? _sanitizeUrl : null, ); } } @@ -810,18 +810,14 @@ function walkIcuTree( const hasBinding = !!attr.value.match(BINDING_REGEXP); if (hasBinding) { if (VALID_ATTRS.hasOwnProperty(lowerAttrName)) { - if (URI_ATTRS[lowerAttrName]) { - generateBindingUpdateOpCodes( - update, - attr.value, - newIndex, - attr.name, - 0, - _sanitizeUrl, - ); - } else { - generateBindingUpdateOpCodes(update, attr.value, newIndex, attr.name, 0, null); - } + generateBindingUpdateOpCodes( + update, + attr.value, + newIndex, + attr.name, + 0, + URI_ATTRS[lowerAttrName] ? _sanitizeUrl : null, + ); } else { ngDevMode && console.warn(
packages/core/test/acceptance/i18n_spec.ts+65 −0 modified@@ -3534,6 +3534,71 @@ describe('runtime i18n', () => { 'translatedText value', ); }); + + describe('attribute sanitization', () => { + @Component({template: ''}) + class SanitizeAppComp { + url = 'javascript:alert("oh no")'; + count = 0; + } + + it('should sanitize translated attribute binding', () => { + const fixture = initWithTemplate(SanitizeAppComp, '<a [attr.href]="url" i18n-href></a>'); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize translated property binding', () => { + const fixture = initWithTemplate(SanitizeAppComp, '<a [href]="url" i18n-href></a>'); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize translated interpolation', () => { + const fixture = initWithTemplate(SanitizeAppComp, '<a href="{{url}}" i18n-href></a>'); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize interpolation inside translated element', () => { + const fixture = initWithTemplate(SanitizeAppComp, `<div i18n><a href="{{url}}"></a></div>`); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize attribute binding inside translated element', () => { + const fixture = initWithTemplate( + SanitizeAppComp, + `<div i18n><a [attr.href]="url"></a></div>`, + ); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize property binding inside translated element', () => { + const fixture = initWithTemplate(SanitizeAppComp, `<div i18n><a [href]="url"></a></div>`); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize property binding inside an ICU', () => { + const fixture = initWithTemplate( + SanitizeAppComp, + `<div i18n>{count, plural, + =0 {no <strong>link</strong> yet} + other {{{count}} Here is the <a href="{{url}}">link</a>!} + }</div>`, + ); + + expect(fixture.nativeElement.querySelector('a')).toBeFalsy(); + + fixture.componentInstance.count = 1; + fixture.detectChanges(); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link).toBeTruthy(); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + }); }); function initWithTemplate(compType: Type<any>, template: string) {
224e60ecb1b9fix(core): sanitize translated attribute bindings with interpolations
2 files changed · +74 −13
packages/core/src/render3/i18n/i18n_parse.ts+9 −13 modified@@ -388,7 +388,7 @@ export function i18nAttributesFirstPass(tView: TView, index: number, values: str previousElementIndex, attrName, countBindings(updateOpCodes), - null, + URI_ATTRS[attrName.toLowerCase()] ? _sanitizeUrl : null, ); } } @@ -810,18 +810,14 @@ function walkIcuTree( const hasBinding = !!attr.value.match(BINDING_REGEXP); if (hasBinding) { if (VALID_ATTRS.hasOwnProperty(lowerAttrName)) { - if (URI_ATTRS[lowerAttrName]) { - generateBindingUpdateOpCodes( - update, - attr.value, - newIndex, - attr.name, - 0, - _sanitizeUrl, - ); - } else { - generateBindingUpdateOpCodes(update, attr.value, newIndex, attr.name, 0, null); - } + generateBindingUpdateOpCodes( + update, + attr.value, + newIndex, + attr.name, + 0, + URI_ATTRS[lowerAttrName] ? _sanitizeUrl : null, + ); } else { ngDevMode && console.warn(
packages/core/test/acceptance/i18n_spec.ts+65 −0 modified@@ -3534,6 +3534,71 @@ describe('runtime i18n', () => { 'translatedText value', ); }); + + describe('attribute sanitization', () => { + @Component({template: ''}) + class SanitizeAppComp { + url = 'javascript:alert("oh no")'; + count = 0; + } + + it('should sanitize translated attribute binding', () => { + const fixture = initWithTemplate(SanitizeAppComp, '<a [attr.href]="url" i18n-href></a>'); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize translated property binding', () => { + const fixture = initWithTemplate(SanitizeAppComp, '<a [href]="url" i18n-href></a>'); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize translated interpolation', () => { + const fixture = initWithTemplate(SanitizeAppComp, '<a href="{{url}}" i18n-href></a>'); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize interpolation inside translated element', () => { + const fixture = initWithTemplate(SanitizeAppComp, `<div i18n><a href="{{url}}"></a></div>`); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize attribute binding inside translated element', () => { + const fixture = initWithTemplate( + SanitizeAppComp, + `<div i18n><a [attr.href]="url"></a></div>`, + ); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize property binding inside translated element', () => { + const fixture = initWithTemplate(SanitizeAppComp, `<div i18n><a [href]="url"></a></div>`); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + + it('should sanitize property binding inside an ICU', () => { + const fixture = initWithTemplate( + SanitizeAppComp, + `<div i18n>{count, plural, + =0 {no <strong>link</strong> yet} + other {{{count}} Here is the <a href="{{url}}">link</a>!} + }</div>`, + ); + + expect(fixture.nativeElement.querySelector('a')).toBeFalsy(); + + fixture.componentInstance.count = 1; + fixture.detectChanges(); + const link: HTMLAnchorElement = fixture.nativeElement.querySelector('a'); + expect(link).toBeTruthy(); + expect(link.getAttribute('href')).toMatch(/^unsafe:/); + }); + }); }); function initWithTemplate(compType: Type<any>, template: string) {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/angular/angular/pull/67541nvdIssue TrackingPatchWEB
- github.com/angular/angular/pull/67561nvdIssue TrackingPatchWEB
- github.com/advisories/GHSA-g93w-mfhg-p222ghsaADVISORY
- github.com/angular/angular/security/advisories/GHSA-g93w-mfhg-p222nvdVendor AdvisoryMitigationWEB
- nvd.nist.gov/vuln/detail/CVE-2026-32635ghsaADVISORY
- github.com/angular/angular/commit/224e60ecb1b90115baa702f1c06edc1d64d86187ghsaWEB
- github.com/angular/angular/commit/78dea55351fb305b33a919c43a6b363137eca166ghsaWEB
- github.com/angular/angular/commit/8630319f74c9575a21693d875cc7d5252516146dghsaWEB
- github.com/angular/angular/commit/ed2d324f9cc12aab6cfa0569ef10b73243a62c65ghsaWEB
News mentions
0No linked articles in our index yet.