Repository navigation
Description
Which @angular/* package(s) are the source of the bug?
core / compiler
Is this a regression?
No
Description
SECURITY_SCHEMA registers one attribute for the SVG namespace, on <a> (dom_security_schema.ts#L106):
registerContext(SecurityContext.URL, SVG_NAMESPACE, [['a', ['href', 'xlink:href']]]);use is not there, so securityContext(':svg:use', 'href', true) lands on checkSecurityContext('use', 'href', 'svg') and returns SecurityContext.NONE, and 'xlink:href'
resolves through the same lookup to the same thing. The compiler emits ɵɵattribute with the sanitizer slot empty, and its own checked-in golden uses this element and shows it
(namespace_attr.js#L7):
$r3$.ɵɵattribute('href', ctx.value, null, 'xlink');
// ^^^^ the sanitizer argument<use href> fetches a document and clones it into the page, and handlers and hyperlinks in that clone are live, so the attribute does what iframe[src], object[data] and embed[src] do. Those three are RESOURCE_URL. That one missing schema entry is the whole defect, and since every spelling of the binding fails through the same lookup, one entry fixes all of them.
A dangerous form is the one where the bound value reaches the document part of the URL, and [attr.href] / [attr.xlink:href] is the only way to bind this attribute, so there is no other syntax an application can move to.
Route A, a data: URL in any field the app binds. An
attacker's: one stored text field that the app later binds is the entire vector.
<!-- iconUrl = 'data:image/svg+xml;base64,' + btoa(profile.icon) + '#x' -->
<svg><use [attr.href]="iconUrl"></use></svg>It executed on Chrome 116 and on Firefox 104 to 121, and on Chrome 113, 114 and 115, with
15, 20 and 16 hits out of 24 launches against 24 of 24 on 116. I bound it as a literal string and as one
built with btoa() at runtime, with the same result. On WebKit no data: URL reached the document in 30
spellings and the <use> box stayed 0x0, so there the refusal is at the load and not at the handler.
Route B, bytes at a URL on the app's own origin. Two ways to put them there, an upload the app serves
back or any endpoint whose response parses as XML and carries the attacker's <svg> somewhere inside it:
<svg><use [attr.href]="iconUrl"></use></svg>
<!-- iconUrl = '/uploads/avatar.png#x' -->
<svg><use [attr.xlink:href]="iconUrl"></use></svg>
<!-- the more common spelling in real code -->
<svg><use [attr.href]="apiUrl"></use></svg>
<!-- apiUrl = '/api/profile.xml#x', no upload -->
<svg><use [attr.href]="assetUrl"></use></svg>
<!-- assetUrl = '/asset.svg', WebKit from 26.5 --><use> resolves the fragment to an element id, so the response does not have to be an SVG file. 9 of the 12 envelopes I served through the app's own /api/profile.xml ran on Chrome 121, and the same 9 onWebKit: a feed, a SOAP body, a sitemap, an XML API response, an XHTML page, a record list, an <?xml?>prologue, bytes after the root element, and the <svg> four levels deep in an unrelated namespace. Firefox 156 stops at the trailing bytes, so it ran 8; a JSON envelope, bytes before the root and no
xmlns never ran.
Route B needs from that response is the follow Same-origin, with or without CORS, and a 2xx status, on every engine.
Chrome and Firefox also require an XML or SVG media type and a fragment in the URL. WebKit never checks the media type: the same bytes run as image/png, text/plain,
application/octet-stream and text/html, which are the four a team picks on purpose to be safe. And from 26.5 it needs no fragment either, which is the fragmentless shape above. Content-Disposition: attachment, X-Content-Type-Options: nosniff, a .png path and a CSP on the response itself change nothing anywhere, and that is the usual "serve user uploads safely" configuration, the default in Rails ActiveStorage proxy mode and Django FileResponse(as_attachment=True). On WebKit nothing on the response blocks it, and the only control left is serving uploads from another origin.
By browser build:
| Browser | Route A | Route B |
|---|---|---|
| Chrome 113–115 | yes | no |
| Chrome 116 | yes | yes |
| Chrome 117–121 | no | yes |
| Chrome 122–158 | no | no, the sprite renders |
| Firefox 104–121 | yes | yes |
| Firefox 122–159 | no | yes |
| WebKit 16.0–27.2 | no | yes |
Firefox still runs Route B, and Mozilla closed bug 1912763 as INVALID because these handlers only run when everything is same-origin, which an app serving user SVG from its own origin is.
Two things in the clone are a different mechanism rather than another route. The cloned <a> is not a handler at all but a live hyperlink, so <a href="javascript:"> runs on a trusted click on Chrome 115 and 122, where every handler sink is inert, which puts its window at 115 to 122 rather than 116 to 121. And <foreignObject><details open="open" ontoggle> fires with no interaction and no resource load, which is what defeats stripping on* from uploads or refusing <image>.
Please provide a link to a minimal reproduction of the bug
See https://github.com/SkyZeroZx/ng-bypass-svg-xss
You can run
npm install
npm run fixtures
npm run matrix -- --angular 22 --chrome 121 --firefox 121 --suite floor
npm run matrix -- --angular 22 --chrome 121 --suite xmlPlease provide the environment you discovered this bug in (run ng version)
Angular: 20.3.34, 21.2.25, 22.2.3
Node: 24.16.0
OS: Windows 11 x64
Chrome for Testing 113–123, 125, 130, 135, 140, 145, 150, 155–158; Chromium 107–116, 121, 122
Firefox 104, 112, 115, 117–123, 133, 138, 143, 148, 153, 156, 159; ESR 115, 140, 153
WebKit 16.0 to 27.2, 19 Playwright builds over 11 versions (not Safari itself)
Anything else?
All tests were performed on Windows, so I can't give a completely reliable guarantee for Safari. I believe the WebKit validation should be relatively reliable, and I would appreciate it if someone could validate it manually. (I have validated this completely manually in both Chrome and Firefox.)
Activity
fix(compiler): sanitize href and xlink:href bindings on SVG use elements
[-]SVG `<use> `href` bindings bypass sanitization allowing XSS[/-][+]SVG `<use>` `href` bindings bypass sanitization allowing XSS[/+]fix(compiler): sanitize href and xlink:href bindings on SVG use elements
fix(compiler): sanitize href and xlink:href bindings on SVG use elements
fix(compiler): sanitize href and xlink:href bindings on SVG use elements