Angular SSR MathML TemplateRef projection bypasses SVG animation validation and enables XSS after 7168bed
Which @angular/* package(s) are the source of the bug?
compiler, core, platform-server
Is this a regression?
No
Description
Summary
I was referred here by Google VRP (Issue 559536002) to disclose this issue to the Angular maintainers.
Angular validates an embedded template using the namespace where the template is declared, but Angular SSR serializes its instantiated nodes at the final insertion location.
A <set> element declared inside an <ng-template> under MathML is validated as MathML. The same TemplateRef can then be instantiated through NgTemplateOutlet inside an SVG link.
Angular SSR emits the attacker-controlled to="javascript:..." value without rejecting or sanitizing it. When Chromium parses the SSR output, <set> becomes an SVG animation element. A normal click on the rendered SVG link executes the JavaScript in the application's origin.
The application template remains trusted. Only the value bound to [attr.to] needs to come from an untrusted API response, stored record, URL parameter, or another normal application data source.
Expected behavior
Angular should reject or safely sanitize sensitive SVG animation bindings even when the element was originally declared under MathML and later instantiated inside SVG.
Actual behavior
The template passes strict AOT compilation.
Angular SSR outputs the raw javascript: value.
Chromium reparses the element in the SVG namespace, and the JavaScript executes after the rendered SVG link is clicked.
Steps to reproduce
Extract the attached ZIP and run:
npm install --ignore-scripts --no-audit --no-fund
npm run aot
python -m http.server 8877 --bind 127.0.0.1
Keep that terminal running. Open another terminal in the same directory and run:
npm run capture
Expected vulnerable result:
SSR_CONTAINS_RAW_JAVASCRIPT_URL=true
SSR_CONTAINS_PROJECTED_SET=true
DIALOG={"type":"alert","message":"127.0.0.1"}
AFTER={"result":"ANGULAR_MATHML_PROJECTION_XSS_EXECUTED","marker":"ANGULAR_MATHML_PROJECTION_XSS_EXECUTED"}
POC_RESULT=VULNERABLE
To see the real browser dialog, run:
npm run capture:visible
Affected versions tested
- Angular 22.1.5
- Angular 22.2.0-next.5, which contains commit
7168bed663ba2a53f087a68a251e8ccfd938eb97
Root cause
Commit 7168bed663ba2a53f087a68a251e8ccfd938eb97 added protection for sensitive SVG animation attributes when the original namespace is absent. It does not apply the same protection when the element has an explicit MathML namespace during compilation.
The TemplateRef is therefore accepted as MathML at compile time but is serialized inside SVG by Angular SSR. The browser then interprets the element using the final SVG namespace.
Relationship to issue #70490
Issue https://github.com/angular/angular/issues/70490 covers an animation element created without an explicit namespace.
This case is different: the element has an explicit MathML namespace during compilation and is later instantiated through TemplateRef / NgTemplateOutlet inside SVG.
Therefore, this path remains reproducible after commit 7168bed.
Suggested remediation
Sensitive SVG animation element and attribute combinations should not be allowed solely because the declaration-time namespace is MathML when the resulting view can be instantiated or serialized inside SVG.
An AOT and SSR regression test should declare the payload TemplateRef under MathML, instantiate it through NgTemplateOutlet inside SVG, parse the serialized HTML, and verify that the unsafe binding cannot reach the browser DOM.
angular-mathml-template-outlet-svg-xss-poc.zip
Please provide a link to a minimal reproduction of the bug
No response
Please provide the exception or error you saw
No exception is thrown.
Angular successfully completes strict AOT compilation and SSR rendering.
The security failure is that the SSR output retains the raw javascript: animation value. Chromium reparses the projected <set> element in the SVG namespace, and the value executes after the rendered SVG link is clicked.
Decisive output:
SSR_CONTAINS_RAW_JAVASCRIPT_URL=true
DIALOG={"type":"alert","message":"127.0.0.1"}
POC_RESULT=VULNERABLE
Please provide the environment you discovered this bug in (run ng version)
Angular CLI: Not used
Node: 24.15.0
Package Manager: npm 11.12.1
OS: Windows 10 x64
Chrome: 152.0.7977.76
Angular: 22.1.5
@angular/common: 22.1.5
@angular/compiler: 22.1.5
@angular/compiler-cli: 22.1.5
@angular/core: 22.1.5
@angular/platform-browser: 22.1.5
@angular/platform-server: 22.1.5
TypeScript: 6.0.2
RxJS: 7.8.2
Also reproduced with Angular 22.2.0-next.5 after commit 7168bed663ba2a53f087a68a251e8ccfd938eb97.
Anything else?
Google VRP Issue 559536002 referred this report to the Angular maintainers for public disclosure and remediation.
Related public issue: https://github.com/angular/angular/issues/70490
Relevant incomplete fix: https://github.com/angular/angular/commit/7168bed663ba2a53f087a68a251e8ccfd938eb97
The attached ZIP contains the complete source code, pinned dependencies, exact reproduction commands, automated browser verification, JSON evidence, and four ordered screenshots.
The PoC uses only a harmless alert(document.domain) and a DOM marker to demonstrate JavaScript execution.
Screenshot evidence
step_1_source_template.png— Exact PoC source showing the MathML-declared TemplateRef, its SVG insertion point, and the harmless JavaScript execution marker.step_2_ssr_before_click.png— Angular SSR output before interaction. The payload has not executed yet.step_3_alert_document_domain.png— Real Chrome alert produced after clicking the projected SVG action. The value127.0.0.1comes fromdocument.domain.step_4_after_alert.png— The live page after execution, showing the verifiedANGULAR_MATHML_PROJECTION_XSS_EXECUTEDmarker.
Screenshot 1 — PoC source and injection path
Lines 60–63 define the attacker-controlled target value, including the harmless javascript:alert(document.domain) proof.
Line 50 binds this value to [attr.to] on a <set> declared inside the MathML TemplateRef. Line 56 passes that template to svg-wrapper, where lines 24–31 instantiate it inside SVG.
Angular SSR retains the raw value, and Chromium interprets <set> as an SVG animation element. Clicking the rendered SVG link then executes the payload in the page's origin.
Source: angular/angular