Skip to content

SVG <use> href bindings bypass sanitization allowing XSS #71618

Open

Description

@SkyZeroZx

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 xml

Please 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

  1. added
    area: securityIssues related to built-in security features, such as HTML sanitation
    on Oct 26, 2026
  2. added this to the needsTriage milestone on Oct 26, 2026
  3. added this to the needsTriage milestone on Oct 26, 2026
  4. added a commit that references this issue on Oct 26, 2026
    e0a1ca6
  5. changed the title [-]SVG `<use> `href` bindings bypass sanitization allowing XSS[/-] [+]SVG `<use>` `href` bindings bypass sanitization allowing XSS[/+] on Oct 26, 2026
  6. added 2 commits that reference this issue on Oct 27, 2026
    93b351c
    160e1ce
  7. added a commit that references this issue on Nov 11, 2026
    0382c25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: securityIssues related to built-in security features, such as HTML sanitation

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions