VYPR
High severityNVD Advisory· Published Dec 1, 2025· Updated Dec 2, 2025

Angular Stored XSS Vulnerability via SVG Animation, SVG URL and MathML Attributes

CVE-2025-66412

Description

Angular is a development platform for building mobile and desktop web applications using TypeScript/JavaScript and other languages. Prior to 21.0.2, 20.3.15, and 19.2.17, A Stored Cross-Site Scripting (XSS) vulnerability has been identified in the Angular Template Compiler. It occurs because the compiler's internal security schema is incomplete, allowing attackers to bypass Angular's built-in security sanitization. Specifically, the schema fails to classify certain URL-holding attributes (e.g., those that could contain javascript: URLs) as requiring strict URL security, enabling the injection of malicious scripts. This vulnerability is fixed in 21.0.2, 20.3.15, and 19.2.17.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@angular/compilernpm
>= 21.0.0-next.0, < 21.0.221.0.2
@angular/compilernpm
>= 20.0.0-next.0, < 20.3.1520.3.15
@angular/compilernpm
>= 19.0.0-next.0, < 19.2.1719.2.17
@angular/compilernpm
<= 18.2.14

Affected products

1

Patches

1
1c6b0704fb63

fix(compiler): prevent XSS via SVG animation `attributeName` and MathML/SVG URLs

https://github.com/angular/angularAlan AgiusDec 1, 2025via ghsa
18 files changed · +323 150
  • goldens/public-api/core/errors.api.md+2 0 modified
    @@ -186,6 +186,8 @@ export const enum RuntimeErrorCode {
         // (undocumented)
         UNKNOWN_ELEMENT = 304,
         // (undocumented)
    +    UNSAFE_ATTRIBUTE_BINDING = -910,
    +    // @deprecated (undocumented)
         UNSAFE_IFRAME_ATTRS = -910,
         // (undocumented)
         UNSAFE_VALUE_IN_RESOURCE_URL = 904,
    
  • packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/iframe_attrs.js+1 1 modified
    @@ -3,6 +3,6 @@ template: function MyComponent_Template(rf, ctx) {
       if (rf & 1) {
         i0.ɵɵelement(0, "iframe", 0);
       } if (rf & 2) {
    -    i0.ɵɵattribute("fetchpriority", "low", i0.ɵɵvalidateIframeAttribute)("allowfullscreen", ctx.fullscreen, i0.ɵɵvalidateIframeAttribute);
    +    i0.ɵɵattribute("fetchpriority", "low", i0.ɵɵvalidateAttribute)("allowfullscreen", ctx.fullscreen, i0.ɵɵvalidateAttribute);
       }
     }
    
  • packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js+25 1 modified
    @@ -923,10 +923,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
     export class HostBindingIframeDir {
         constructor() {
             this.evil = 'evil';
    +        this.nonEvil = 'nonEvil';
         }
     }
     HostBindingIframeDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingIframeDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
    -HostBindingIframeDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingIframeDir, isStandalone: true, selector: "iframe[hostBindingIframeDir]", host: { properties: { "innerHtml": "evil", "attr.style": "evil", "src": "evil", "sandbox": "evil" } }, ngImport: i0 });
    +HostBindingIframeDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingIframeDir, isStandalone: true, selector: "iframe[hostBindingIframeDir]", host: { properties: { "innerHtml": "evil", "attr.style": "evil", "src": "evil", "sandbox": "evil", "attr.attributeName": "nonEvil" } }, ngImport: i0 });
     i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingIframeDir, decorators: [{
                 type: Directive,
                 args: [{
    @@ -936,6 +937,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
                             '[attr.style]': 'evil',
                             '[src]': 'evil',
                             '[sandbox]': 'evil',
    +                        '[attr.attributeName]': 'nonEvil',
    +                    },
    +                }]
    +        }] });
    +export class HostBindingSvgAnimateDir {
    +    constructor() {
    +        this.evil = 'evil';
    +    }
    +}
    +HostBindingSvgAnimateDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingSvgAnimateDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
    +HostBindingSvgAnimateDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingSvgAnimateDir, isStandalone: true, selector: "animateMotion[hostBindingSvgAnimateDir]", host: { properties: { "attr.attributeName": "evil" } }, ngImport: i0 });
    +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingSvgAnimateDir, decorators: [{
    +            type: Directive,
    +            args: [{
    +                    selector: 'animateMotion[hostBindingSvgAnimateDir]',
    +                    host: {
    +                        '[attr.attributeName]': 'evil',
                         },
                     }]
             }] });
    @@ -956,9 +974,15 @@ export declare class HostBindingImageDir {
     }
     export declare class HostBindingIframeDir {
         evil: string;
    +    nonEvil: string;
         static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingIframeDir, never>;
         static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingIframeDir, "iframe[hostBindingIframeDir]", never, {}, {}, never, never, true, never>;
     }
    +export declare class HostBindingSvgAnimateDir {
    +    evil: string;
    +    static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingSvgAnimateDir, never>;
    +    static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingSvgAnimateDir, "animateMotion[hostBindingSvgAnimateDir]", never, {}, {}, never, never, true, never>;
    +}
     
     /****************************************************************************************************
      * PARTIAL FILE: security_sensitive_constant_attributes.js
    
  • packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.js+8 2 modified
    @@ -14,7 +14,13 @@ hostBindings: function HostBindingImageDir_HostBindings(rf, ctx) {
    
     hostBindings: function HostBindingIframeDir_HostBindings(rf, ctx) {
       if (rf & 2) {
    -    $r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("src", ctx.evil, $r3$.ɵɵsanitizeResourceUrl)("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute);
    -    $r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle);
    +    $r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("src", ctx.evil, $r3$.ɵɵsanitizeResourceUrl)("sandbox", ctx.evil, $r3$.ɵɵvalidateAttribute);
    +    $r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle)("attributeName", ctx.nonEvil);
       }
     }
    +…
    +hostBindings: function HostBindingSvgAnimateDir_HostBindings(rf, ctx) {
    +  if (rf & 2) {
    +    i0.ɵɵattribute("attributeName", ctx.evil, i0.ɵɵvalidateAttribute);
    +  }
    +} 
    
  • packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.ts+12 0 modified
    @@ -31,8 +31,20 @@ export class HostBindingImageDir {
         '[attr.style]': 'evil',
         '[src]': 'evil',
         '[sandbox]': 'evil',
    +    '[attr.attributeName]': 'nonEvil',
       },
     })
     export class HostBindingIframeDir {
       evil = 'evil';
    +  nonEvil = 'nonEvil';
    +}
    +
    +@Directive({
    +  selector: 'animateMotion[hostBindingSvgAnimateDir]',
    +  host: {
    +    '[attr.attributeName]': 'evil',
    +  },
    +})
    +export class HostBindingSvgAnimateDir {
    +  evil = 'evil';
     }
    
  • packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/property_bindings/sanitization.js+1 1 modified
    @@ -11,7 +11,7 @@ template: function MyComponent_Template(rf, ctx) {
         $r3$.ɵɵadvance();
         $r3$.ɵɵdomProperty("src", ctx.evil, $r3$.ɵɵsanitizeUrl);
         $r3$.ɵɵadvance();
    -    $r3$.ɵɵdomProperty("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute);
    +    $r3$.ɵɵdomProperty("sandbox", ctx.evil, $r3$.ɵɵvalidateAttribute);
         $r3$.ɵɵadvance();
         $r3$.ɵɵdomProperty("href", $r3$.ɵɵinterpolate2("", ctx.evil, "", ctx.evil), $r3$.ɵɵsanitizeUrl);
         $r3$.ɵɵadvance();
    
  • packages/compiler-cli/test/ngtsc/ngtsc_spec.ts+33 6 modified
    @@ -9258,6 +9258,33 @@ runInEachFileSystem((os: string) => {
           });
         });
     
    +    describe('SVG animation processing', () => {
    +      it('should generate SVG animation validation instruction', () => {
    +        env.write(
    +          'test.ts',
    +          `
    +            import {Component} from '@angular/core';
    +
    +            @Component({
    +              selector: 'test-cmp',
    +              template: '<svg><animate [attr.attributeName]="attr"></animate></svg>',
    +              standalone: false,
    +            })
    +            export class TestCmp {
    +              attr = 'opacity';
    +            }
    +          `,
    +        );
    +
    +        env.driveMain();
    +
    +        const jsContents = env.getContents('test.js');
    +        expect(jsContents).toContain(
    +          'i0.ɵɵattribute("attributeName", ctx.attr, i0.ɵɵvalidateAttribute);',
    +        );
    +      });
    +    });
    +
         describe('inline resources', () => {
           it('should process inline <style> tags', () => {
             env.write(
    @@ -9679,11 +9706,11 @@ runInEachFileSystem((os: string) => {
             // Only `sandbox` has an extra validation fn (since it's security-sensitive),
             // the `title` property doesn't have an extra validation fn.
             expect(jsContents).toContain(
    -          'ɵɵdomProperty("sandbox", "", i0.ɵɵvalidateIframeAttribute)("title", "Hi!")',
    +          'ɵɵdomProperty("sandbox", "", i0.ɵɵvalidateAttribute)("title", "Hi!")',
             );
     
             // The `allow` property is also security-sensitive, thus an extra validation fn.
    -        expect(jsContents).toContain('ɵɵattribute("allow", "", i0.ɵɵvalidateIframeAttribute)');
    +        expect(jsContents).toContain('ɵɵattribute("allow", "", i0.ɵɵvalidateAttribute)');
           });
     
           it(
    @@ -9713,7 +9740,7 @@ runInEachFileSystem((os: string) => {
               // Make sure that the `sandbox` has an extra validation fn,
               // and the check is case-insensitive (since the `setAttribute` DOM API
               // is case-insensitive as well).
    -          expect(jsContents).toContain('ɵɵattribute("SANDBOX", "", i0.ɵɵvalidateIframeAttribute)');
    +          expect(jsContents).toContain('ɵɵattribute("SANDBOX", "", i0.ɵɵvalidateAttribute)');
             },
           );
     
    @@ -9771,11 +9798,11 @@ runInEachFileSystem((os: string) => {
             // The `sandbox` is potentially a security-sensitive attribute of an <iframe>.
             // Generate an extra validation function to invoke at runtime, which would
             // check if an underlying host element is an <iframe>.
    -        expect(jsContents).toContain('ɵɵdomProperty("sandbox", "", i0.ɵɵvalidateIframeAttribute)');
    +        expect(jsContents).toContain('ɵɵdomProperty("sandbox", "", i0.ɵɵvalidateAttribute)');
     
             // Similar to the above, but for an attribute binding (host attributes are
             // represented via `ɵɵattribute`).
    -        expect(jsContents).toContain('ɵɵattribute("allow", "", i0.ɵɵvalidateIframeAttribute)');
    +        expect(jsContents).toContain('ɵɵattribute("allow", "", i0.ɵɵvalidateAttribute)');
           });
     
           it(
    @@ -9801,7 +9828,7 @@ runInEachFileSystem((os: string) => {
     
               // Make sure that we generate a validation fn for the `sandbox` attribute,
               // even when it was declared as `SANDBOX`.
    -          expect(jsContents).toContain('ɵɵattribute("SANDBOX", "", i0.ɵɵvalidateIframeAttribute)');
    +          expect(jsContents).toContain('ɵɵattribute("SANDBOX", "", i0.ɵɵvalidateAttribute)');
             },
           );
         });
    
  • packages/compiler/src/core.ts+1 0 modified
    @@ -84,6 +84,7 @@ export enum SecurityContext {
       SCRIPT = 3,
       URL = 4,
       RESOURCE_URL = 5,
    +  ATTRIBUTE_NO_BINDING = 6,
     }
     
     /**
    
  • packages/compiler/src/render3/r3_identifiers.ts+4 4 modified
    @@ -455,6 +455,10 @@ export class Identifiers {
       // sanitization-related functions
       static sanitizeHtml: o.ExternalReference = {name: 'ɵɵsanitizeHtml', moduleName: CORE};
       static sanitizeStyle: o.ExternalReference = {name: 'ɵɵsanitizeStyle', moduleName: CORE};
    +  static validateAttribute: o.ExternalReference = {
    +    name: 'ɵɵvalidateAttribute',
    +    moduleName: CORE,
    +  };
       static sanitizeResourceUrl: o.ExternalReference = {
         name: 'ɵɵsanitizeResourceUrl',
         moduleName: CORE,
    @@ -470,10 +474,6 @@ export class Identifiers {
         name: 'ɵɵtrustConstantResourceUrl',
         moduleName: CORE,
       };
    -  static validateIframeAttribute: o.ExternalReference = {
    -    name: 'ɵɵvalidateIframeAttribute',
    -    moduleName: CORE,
    -  };
     
       // Decorators
       static inputDecorator: o.ExternalReference = {name: 'Input', moduleName: CORE};
    
  • packages/compiler/src/schema/dom_security_schema.ts+100 29 modified
    @@ -15,7 +15,6 @@ import {SecurityContext} from '../core';
     // =================================================================================================
     //
     //        DO NOT EDIT THIS LIST OF SECURITY SENSITIVE PROPERTIES WITHOUT A SECURITY REVIEW!
    -//                               Reach out to mprobst for details.
     //
     // =================================================================================================
     
    @@ -36,6 +35,7 @@ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} {
           'area|ping',
           'audio|src',
           'a|href',
    +      'a|xlink:href',
           'a|ping',
           'blockquote|cite',
           'body|background',
    @@ -49,7 +49,77 @@ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} {
           'track|src',
           'video|poster',
           'video|src',
    +
    +      // MathML namespace
    +      // https://crsrc.org/c/third_party/blink/renderer/core/sanitizer/sanitizer.cc;l=753-768;drc=b3eb16372dcd3317d65e9e0265015e322494edcd;bpv=1;bpt=1
    +      'annotation|href',
    +      'annotation|xlink:href',
    +      'annotation-xml|href',
    +      'annotation-xml|xlink:href',
    +      'maction|href',
    +      'maction|xlink:href',
    +      'malignmark|href',
    +      'malignmark|xlink:href',
    +      'math|href',
    +      'math|xlink:href',
    +      'mroot|href',
    +      'mroot|xlink:href',
    +      'msqrt|href',
    +      'msqrt|xlink:href',
    +      'merror|href',
    +      'merror|xlink:href',
    +      'mfrac|href',
    +      'mfrac|xlink:href',
    +      'mglyph|href',
    +      'mglyph|xlink:href',
    +      'msub|href',
    +      'msub|xlink:href',
    +      'msup|href',
    +      'msup|xlink:href',
    +      'msubsup|href',
    +      'msubsup|xlink:href',
    +      'mmultiscripts|href',
    +      'mmultiscripts|xlink:href',
    +      'mprescripts|href',
    +      'mprescripts|xlink:href',
    +      'mi|href',
    +      'mi|xlink:href',
    +      'mn|href',
    +      'mn|xlink:href',
    +      'mo|href',
    +      'mo|xlink:href',
    +      'mpadded|href',
    +      'mpadded|xlink:href',
    +      'mphantom|href',
    +      'mphantom|xlink:href',
    +      'mrow|href',
    +      'mrow|xlink:href',
    +      'ms|href',
    +      'ms|xlink:href',
    +      'mspace|href',
    +      'mspace|xlink:href',
    +      'mstyle|href',
    +      'mstyle|xlink:href',
    +      'mtable|href',
    +      'mtable|xlink:href',
    +      'mtd|href',
    +      'mtd|xlink:href',
    +      'mtr|href',
    +      'mtr|xlink:href',
    +      'mtext|href',
    +      'mtext|xlink:href',
    +      'mover|href',
    +      'mover|xlink:href',
    +      'munder|href',
    +      'munder|xlink:href',
    +      'munderover|href',
    +      'munderover|xlink:href',
    +      'semantics|href',
    +      'semantics|xlink:href',
    +      'none|href',
    +      'none|xlink:href',
         ]);
    +
         registerContext(SecurityContext.RESOURCE_URL, [
           'applet|code',
           'applet|codebase',
    @@ -65,38 +135,39 @@ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} {
           'object|data',
           'script|src',
         ]);
    +
    +    // Keep this in sync with SECURITY_SENSITIVE_ELEMENTS in packages/core/src/sanitization/sanitization.ts
    +    // Unknown is the internal tag name for unknown elements example used for host-bindings.
    +    // These are unsafe as `attributeName` can be `href` or `xlink:href`
    +    // See: http://b/463880509#comment7
    +
    +    registerContext(SecurityContext.ATTRIBUTE_NO_BINDING, [
    +      'animate|attributeName',
    +      'set|attributeName',
    +      'animateMotion|attributeName',
    +      'animateTransform|attributeName',
    +
    +      'unknown|attributeName',
    +
    +      'iframe|sandbox',
    +      'iframe|allow',
    +      'iframe|allowFullscreen',
    +      'iframe|referrerPolicy',
    +      'iframe|csp',
    +      'iframe|fetchPriority',
    +
    +      'unknown|sandbox',
    +      'unknown|allow',
    +      'unknown|allowFullscreen',
    +      'unknown|referrerPolicy',
    +      'unknown|csp',
    +      'unknown|fetchPriority',
    +    ]);
       }
    +
       return _SECURITY_SCHEMA;
     }
     
     function registerContext(ctx: SecurityContext, specs: string[]) {
       for (const spec of specs) _SECURITY_SCHEMA[spec.toLowerCase()] = ctx;
     }
    -
    -/**
    - * The set of security-sensitive attributes of an `<iframe>` that *must* be
    - * applied as a static attribute only. This ensures that all security-sensitive
    - * attributes are taken into account while creating an instance of an `<iframe>`
    - * at runtime.
    - *
    - * Note: avoid using this set directly, use the `isIframeSecuritySensitiveAttr` function
    - * in the code instead.
    - */
    -export const IFRAME_SECURITY_SENSITIVE_ATTRS = new Set([
    -  'sandbox',
    -  'allow',
    -  'allowfullscreen',
    -  'referrerpolicy',
    -  'csp',
    -  'fetchpriority',
    -]);
    -
    -/**
    - * Checks whether a given attribute name might represent a security-sensitive
    - * attribute of an <iframe>.
    - */
    -export function isIframeSecuritySensitiveAttr(attrName: string): boolean {
    -  // The `setAttribute` DOM API is case-insensitive, so we lowercase the value
    -  // before checking it against a known security-sensitive attributes.
    -  return IFRAME_SECURITY_SENSITIVE_ATTRS.has(attrName.toLowerCase());
    -}
    
  • packages/compiler/src/template/pipeline/src/phases/resolve_sanitizers.ts+4 38 modified
    @@ -9,10 +9,8 @@
     import {SecurityContext} from '../../../../core';
     import * as o from '../../../../output/output_ast';
     import {Identifiers} from '../../../../render3/r3_identifiers';
    -import {isIframeSecuritySensitiveAttr} from '../../../../schema/dom_security_schema';
     import * as ir from '../../ir';
     import {CompilationJob, CompilationJobKind} from '../compilation';
    -import {createOpXrefMap} from '../util/elements';
     
     /**
      * Map of security contexts to their sanitizer function.
    @@ -23,6 +21,7 @@ const sanitizerFns = new Map<SecurityContext, o.ExternalReference>([
       [SecurityContext.SCRIPT, Identifiers.sanitizeScript],
       [SecurityContext.STYLE, Identifiers.sanitizeStyle],
       [SecurityContext.URL, Identifiers.sanitizeUrl],
    +  [SecurityContext.ATTRIBUTE_NO_BINDING, Identifiers.validateAttribute],
     ]);
     
     /**
    @@ -38,8 +37,6 @@ const trustedValueFns = new Map<SecurityContext, o.ExternalReference>([
      */
     export function resolveSanitizers(job: CompilationJob): void {
       for (const unit of job.units) {
    -    const elements = createOpXrefMap(unit);
    -
         // For normal element bindings we create trusted values for security sensitive constant
         // attributes. However, for host bindings we skip this step (this matches what
         // TemplateDefinitionBuilder does).
    @@ -63,8 +60,8 @@ export function resolveSanitizers(job: CompilationJob): void {
               if (
                 Array.isArray(op.securityContext) &&
                 op.securityContext.length === 2 &&
    -            op.securityContext.indexOf(SecurityContext.URL) > -1 &&
    -            op.securityContext.indexOf(SecurityContext.RESOURCE_URL) > -1
    +            op.securityContext.includes(SecurityContext.URL) &&
    +            op.securityContext.includes(SecurityContext.RESOURCE_URL)
               ) {
                 // When the host element isn't known, some URL attributes (such as "src" and "href") may
                 // be part of multiple different security contexts. In this case we use special
    @@ -74,46 +71,15 @@ export function resolveSanitizers(job: CompilationJob): void {
               } else {
                 sanitizerFn = sanitizerFns.get(getOnlySecurityContext(op.securityContext)) ?? null;
               }
    +
               op.sanitizer = sanitizerFn !== null ? o.importExpr(sanitizerFn) : null;
     
    -          // If there was no sanitization function found based on the security context of an
    -          // attribute/property, check whether this attribute/property is one of the
    -          // security-sensitive <iframe> attributes (and that the current element is actually an
    -          // <iframe>).
    -          if (op.sanitizer === null) {
    -            let isIframe = false;
    -            if (job.kind === CompilationJobKind.Host || op.kind === ir.OpKind.DomProperty) {
    -              // Note: for host bindings defined on a directive, we do not try to find all
    -              // possible places where it can be matched, so we can not determine whether
    -              // the host element is an <iframe>. In this case, we just assume it is and append a
    -              // validation function, which is invoked at runtime and would have access to the
    -              // underlying DOM element to check if it's an <iframe> and if so - run extra checks.
    -              isIframe = true;
    -            } else {
    -              // For a normal binding we can just check if the element its on is an iframe.
    -              const ownerOp = elements.get(op.target);
    -              if (ownerOp === undefined || !ir.isElementOrContainerOp(ownerOp)) {
    -                throw Error('Property should have an element-like owner');
    -              }
    -              isIframe = isIframeElement(ownerOp);
    -            }
    -            if (isIframe && isIframeSecuritySensitiveAttr(op.name)) {
    -              op.sanitizer = o.importExpr(Identifiers.validateIframeAttribute);
    -            }
    -          }
               break;
           }
         }
       }
     }
     
    -/**
    - * Checks whether the given op represents an iframe element.
    - */
    -function isIframeElement(op: ir.ElementOrContainerOps): boolean {
    -  return op.kind === ir.OpKind.ElementStart && op.tag?.toLowerCase() === 'iframe';
    -}
    -
     /**
      * Asserts that there is only a single security context and returns it.
      */
    
  • packages/compiler/test/security_spec.ts+0 28 removed
    @@ -1,28 +0,0 @@
    -/**
    - * @license
    - * Copyright Google LLC All Rights Reserved.
    - *
    - * Use of this source code is governed by an MIT-style license that can be
    - * found in the LICENSE file at https://angular.dev/license
    - */
    -
    -import {IFRAME_SECURITY_SENSITIVE_ATTRS, SECURITY_SCHEMA} from '../src/schema/dom_security_schema';
    -
    -describe('security-related tests', () => {
    -  it('should have no overlap between `IFRAME_SECURITY_SENSITIVE_ATTRS` and `SECURITY_SCHEMA`', () => {
    -    // The `IFRAME_SECURITY_SENSITIVE_ATTRS` and `SECURITY_SCHEMA` tokens configure sanitization
    -    // and validation rules and used to pick the right sanitizer function.
    -    // This test verifies that there is no overlap between two sets of rules to flag
    -    // a situation when 2 sanitizer functions may be needed at the same time (in which
    -    // case, compiler logic should be extended to support that).
    -    const schema = new Set();
    -    Object.keys(SECURITY_SCHEMA()).forEach((key: string) => schema.add(key.toLowerCase()));
    -    let hasOverlap = false;
    -    IFRAME_SECURITY_SENSITIVE_ATTRS.forEach((attr) => {
    -      if (schema.has('*|' + attr) || schema.has('iframe|' + attr)) {
    -        hasOverlap = true;
    -      }
    -    });
    -    expect(hasOverlap).toBeFalse();
    -  });
    -});
    
  • packages/core/src/core_render3_private_export.ts+1 1 modified
    @@ -301,8 +301,8 @@ export {
       ɵɵsanitizeUrlOrResourceUrl,
       ɵɵtrustConstantHtml,
       ɵɵtrustConstantResourceUrl,
    +  ɵɵvalidateAttribute,
     } from './sanitization/sanitization';
    -export {ɵɵvalidateIframeAttribute} from './sanitization/iframe_attrs_validation';
     export {noSideEffects as ɵnoSideEffects} from './util/closure';
     export {AfterRenderManager as ɵAfterRenderManager} from './render3/after_render/manager';
     export {depsTracker as ɵdepsTracker} from './render3/deps_tracker/deps_tracker';
    
  • packages/core/src/errors.ts+5 0 modified
    @@ -120,6 +120,11 @@ export const enum RuntimeErrorCode {
       TYPE_IS_NOT_STANDALONE = 907,
       MISSING_ZONEJS = 908,
       UNEXPECTED_ZONE_STATE = 909,
    +  UNSAFE_ATTRIBUTE_BINDING = -910,
    +  /**
    +   * @deprecated use `UNSAFE_ATTRIBUTE_BINDING` instead.
    +   */
    +  // tslint:disable-next-line:no-duplicate-enum-values
       UNSAFE_IFRAME_ATTRS = -910,
       VIEW_ALREADY_DESTROYED = 911,
       COMPONENT_ID_COLLISION = -912,
    
  • packages/core/src/render3/jit/environment.ts+1 1 modified
    @@ -183,11 +183,11 @@ export const angularCoreEnv: {[name: string]: unknown} = (() => ({
       'ɵɵsanitizeStyle': sanitization.ɵɵsanitizeStyle,
       'ɵɵsanitizeResourceUrl': sanitization.ɵɵsanitizeResourceUrl,
       'ɵɵsanitizeScript': sanitization.ɵɵsanitizeScript,
    +  'ɵɵvalidateAttribute': sanitization.ɵɵvalidateAttribute,
       'ɵɵsanitizeUrl': sanitization.ɵɵsanitizeUrl,
       'ɵɵsanitizeUrlOrResourceUrl': sanitization.ɵɵsanitizeUrlOrResourceUrl,
       'ɵɵtrustConstantHtml': sanitization.ɵɵtrustConstantHtml,
       'ɵɵtrustConstantResourceUrl': sanitization.ɵɵtrustConstantResourceUrl,
    -  'ɵɵvalidateIframeAttribute': iframe_attrs_validation.ɵɵvalidateIframeAttribute,
     
       'forwardRef': forwardRef,
       'resolveForwardRef': resolveForwardRef,
    
  • packages/core/src/sanitization/iframe_attrs_validation.ts+15 36 modified
    @@ -6,52 +6,31 @@
      * found in the LICENSE file at https://angular.dev/license
      */
     
    -import {RuntimeError, RuntimeErrorCode} from '../errors';
    -import {getTemplateLocationDetails} from '../render3/instructions/element_validation';
    -import {TNodeType} from '../render3/interfaces/node';
    -import {RComment, RElement} from '../render3/interfaces/renderer_dom';
     import {RENDERER} from '../render3/interfaces/view';
     import {nativeRemoveNode} from '../render3/dom_node_manipulation';
    -import {getLView, getSelectedTNode} from '../render3/state';
    -import {getNativeByTNode} from '../render3/util/view_utils';
    +import {getLView} from '../render3/state';
     import {trustedHTMLFromString} from '../util/security/trusted_types';
     
     /**
    - * Validation function invoked at runtime for each binding that might potentially
    - * represent a security-sensitive attribute of an <iframe>.
    - * See `IFRAME_SECURITY_SENSITIVE_ATTRS` in the
    - * `packages/compiler/src/schema/dom_security_schema.ts` script for the full list
    + * Enforces security by neutralizing an `<iframe>` if a security-sensitive attribute is set.
    + *
    + * This function is invoked at runtime when a security-sensitive attribute is bound to an `<iframe>`.
    + * It clears the `src` and `srcdoc` attributes and removes the `<iframe>` from the DOM to prevent
    + * potential security risks.
    + *
    + * @see [SECURITY_SCHEMA](../../../compiler/src/schema/dom_security_schema.ts) for the full list
      * of such attributes.
      *
      * @codeGenApi
      */
    -export function ɵɵvalidateIframeAttribute(attrValue: any, tagName: string, attrName: string) {
    +export function enforceIframeSecurity(iframe: HTMLIFrameElement): void {
       const lView = getLView();
    -  const tNode = getSelectedTNode()!;
    -  const element = getNativeByTNode(tNode, lView) as RElement | RComment;
    -
    -  // Restrict any dynamic bindings of security-sensitive attributes/properties
    -  // on an <iframe> for security reasons.
    -  if (tNode.type === TNodeType.Element && tagName.toLowerCase() === 'iframe') {
    -    const iframe = element as HTMLIFrameElement;
    -
    -    // Unset previously applied `src` and `srcdoc` if we come across a situation when
    -    // a security-sensitive attribute is set later via an attribute/property binding.
    -    iframe.src = '';
    -    iframe.srcdoc = trustedHTMLFromString('') as unknown as string;
     
    -    // Also remove the <iframe> from the document.
    -    nativeRemoveNode(lView[RENDERER], iframe);
    +  // Unset previously applied `src` and `srcdoc` if we come across a situation when
    +  // a security-sensitive attribute is set later via an attribute/property binding.
    +  iframe.src = '';
    +  iframe.srcdoc = trustedHTMLFromString('') as unknown as string;
     
    -    const errorMessage =
    -      ngDevMode &&
    -      `Angular has detected that the \`${attrName}\` was applied ` +
    -        `as a binding to an <iframe>${getTemplateLocationDetails(lView)}. ` +
    -        `For security reasons, the \`${attrName}\` can be set on an <iframe> ` +
    -        `as a static attribute only. \n` +
    -        `To fix this, switch the \`${attrName}\` binding to a static attribute ` +
    -        `in a template or in host bindings section.`;
    -    throw new RuntimeError(RuntimeErrorCode.UNSAFE_IFRAME_ATTRS, errorMessage);
    -  }
    -  return attrValue;
    +  // Also remove the <iframe> from the document.
    +  nativeRemoveNode(lView[RENDERER], iframe);
     }
    
  • packages/core/src/sanitization/sanitization.ts+67 1 modified
    @@ -8,10 +8,14 @@
     
     import {XSS_SECURITY_URL} from '../error_details_base_url';
     import {RuntimeError, RuntimeErrorCode} from '../errors';
    +import {getTemplateLocationDetails} from '../render3/instructions/element_validation';
     import {getDocument} from '../render3/interfaces/document';
    +import {TNodeType} from '../render3/interfaces/node';
    +import {RElement} from '../render3/interfaces/renderer_dom';
     import {ENVIRONMENT} from '../render3/interfaces/view';
    -import {getLView} from '../render3/state';
    +import {getLView, getSelectedTNode} from '../render3/state';
     import {renderStringify} from '../render3/util/stringify_utils';
    +import {getNativeByTNode} from '../render3/util/view_utils';
     import {TrustedHTML, TrustedScript, TrustedScriptURL} from '../util/security/trusted_type_defs';
     import {trustedHTMLFromString, trustedScriptURLFromString} from '../util/security/trusted_types';
     import {
    @@ -22,6 +26,7 @@ import {
     
     import {allowSanitizationBypassAndThrow, BypassType, unwrapSafeValue} from './bypass';
     import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer';
    +import {enforceIframeSecurity} from './iframe_attrs_validation';
     import {Sanitizer} from './sanitizer';
     import {SecurityContext} from './security';
     import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer';
    @@ -273,3 +278,64 @@ function getSanitizer(): Sanitizer | null {
       const lView = getLView();
       return lView && lView[ENVIRONMENT].sanitizer;
     }
    +
    +const attributeName: ReadonlySet<string> = new Set(['attributename']);
    +
    +/**
    + * @remarks Keep this in sync with DOM Security Schema.
    + * @see [SECURITY_SCHEMA](../../../compiler/src/schema/dom_security_schema.ts)
    + */
    +const SECURITY_SENSITIVE_ELEMENTS: Readonly<Record<string, ReadonlySet<string>>> = {
    +  'iframe': new Set([
    +    'sandbox',
    +    'allow',
    +    'allowfullscreen',
    +    'referrerpolicy',
    +    'csp',
    +    'fetchpriority',
    +  ]),
    +  'animate': attributeName,
    +  'set': attributeName,
    +  'animatemotion': attributeName,
    +  'animatetransform': attributeName,
    +};
    +
    +/**
    + * Validates that the attribute binding is safe to use.
    + *
    + * @param value The value of the attribute.
    + * @param tagName The name of the tag.
    + * @param attributeName The name of the attribute.
    + */
    +export function ɵɵvalidateAttribute(
    +  value: unknown,
    +  tagName: string,
    +  attributeName: string,
    +): unknown {
    +  const lowerCaseTagName = tagName.toLowerCase();
    +  const lowerCaseAttrName = attributeName.toLowerCase();
    +  if (!SECURITY_SENSITIVE_ELEMENTS[lowerCaseTagName]?.has(lowerCaseAttrName)) {
    +    return value;
    +  }
    +
    +  const tNode = getSelectedTNode()!;
    +  if (tNode.type !== TNodeType.Element) {
    +    return value;
    +  }
    +
    +  const lView = getLView();
    +  if (lowerCaseTagName === 'iframe') {
    +    const element = getNativeByTNode(tNode, lView) as RElement;
    +    enforceIframeSecurity(element as HTMLIFrameElement);
    +  }
    +
    +  const errorMessage =
    +    ngDevMode &&
    +    `Angular has detected that the \`${attributeName}\` was applied ` +
    +      `as a binding to the <${tagName}> element${getTemplateLocationDetails(lView)}. ` +
    +      `For security reasons, the \`${attributeName}\` can be set on the <${tagName}> element ` +
    +      `as a static attribute only. \n` +
    +      `To fix this, switch the \`${attributeName}\` binding to a static attribute ` +
    +      `in a template or in host bindings section.`;
    +  throw new RuntimeError(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING, errorMessage);
    +}
    
  • packages/core/test/acceptance/security_spec.ts+43 1 modified
    @@ -67,7 +67,7 @@ describe('iframe processing', () => {
         });
       });
       function getErrorMessageRegexp() {
    -    const errorMessagePart = 'NG0' + Math.abs(RuntimeErrorCode.UNSAFE_IFRAME_ATTRS).toString();
    +    const errorMessagePart = 'NG0' + Math.abs(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING).toString();
         return new RegExp(errorMessagePart);
       }
     
    @@ -726,3 +726,45 @@ describe('iframe processing', () => {
         });
       });
     });
    +
    +describe('SVG animation processing', () => {
    +  it('should error when `attributeName` is bound', () => {
    +    @Component({
    +      template: '<svg><animate [attr.attributeName]="attr"></animate></svg>',
    +    })
    +    class TestCmp {
    +      attr = 'href';
    +    }
    +
    +    expect(() => {
    +      const fixture = TestBed.createComponent(TestCmp);
    +      fixture.detectChanges();
    +    }).toThrowError(
    +      /NG0910: Angular has detected that the `attributeName` was applied as a binding to the <animate>/,
    +    );
    +  });
    +
    +  it(`should error when a directive sets a 'attributeName' as an attribute binding`, () => {
    +    @Directive({
    +      selector: '[dir]',
    +      host: {
    +        '[attr.attributeName]': "'href'",
    +      },
    +    })
    +    class animateAttrDir {}
    +
    +    @Component({
    +      imports: [animateAttrDir],
    +      selector: 'my-comp',
    +      template: '<svg><animate dir></animate></svg>',
    +    })
    +    class TestCmp {}
    +
    +    expect(() => {
    +      const fixture = TestBed.createComponent(TestCmp);
    +      fixture.detectChanges();
    +    }).toThrowError(
    +      /NG0910: Angular has detected that the `attributeName` was applied as a binding to the <animate>/,
    +    );
    +  });
    +});
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.