VYPR
Medium severity5.3GHSA Advisory· Published Jun 15, 2026· Updated Jun 15, 2026

@angular/core: Angular Template and Dynamic Component Namespace Bypass leading to Cross-Site Scripting (XSS)

CVE-2026-52725

Description

Angular's @angular/core fails to reject mounting dynamic components on elements, enabling attacker-controlled XSS via createComponent.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Angular's @angular/core fails to reject mounting dynamic components on elements, enabling attacker-controlled XSS via createComponent.

Vulnerability

In @angular/core versions prior to 22.0.0-rc.2, 21.2.15, 20.3.22, and 19.2.23, the createComponent API does not reject mounting components directly onto a ` or namespaced script element (e.g., <svg:script>`). This allows an attacker to initialize a custom component on a tag that executes scripts, bypassing built-in dynamic component creation safeguards [1][2].

Exploitation

The attacker must have control over the host element or selector parameter passed to createComponent, and the application must not perform additional sanitization on user-supplied inputs before passing them to this API. The attacker provides a selector that resolves to a ` or <svg:script>` element, causing the dynamic component to be mounted on that script host, leading to execution of arbitrary JavaScript in the user's browser [1][2].

Impact

Successful exploitation results in client-side Cross-Site Scripting (XSS), allowing the attacker to execute arbitrary JavaScript within the target user's browser context. This can lead to session hijacking, sensitive data exposure, or unauthorized actions on behalf of the user [1][2].

Mitigation

Angular has released patches in versions 22.0.0-rc.2, 21.2.15, 20.3.22, and 19.2.23 [1][2]. The fix, implemented in pull request #68713 [4], rejects mounting dynamic components on `` elements by throwing a runtime error in development mode. Additionally, pull request #68686 [3] refactors namespace attribute validation to improve security context mapping. Upgrading to a patched version is recommended.

AI Insight generated on Jun 15, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

2
cef4a095a2a6

refactor(core): align namespaced attribute validation and security schema contexts

https://github.com/angular/angularAlan AgiusMay 18, 2026via body-scan-shorthand
13 files changed · +225 166
  • packages/compiler/src/schema/dom_element_schema_registry.ts+8 6 modified
    @@ -448,12 +448,14 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
         // property names do not have a security impact.
         tagName = tagName.toLowerCase();
         propName = propName.toLowerCase();
    -    let ctx = SECURITY_SCHEMA()[tagName + '|' + propName];
    -    if (ctx) {
    -      return ctx;
    -    }
    -    ctx = SECURITY_SCHEMA()['*|' + propName];
    -    return ctx ? ctx : SecurityContext.NONE;
    +
    +    const securitySchema = SECURITY_SCHEMA();
    +    const ctx =
    +      securitySchema[tagName + '|' + propName] ??
    +      securitySchema['*|' + propName] ??
    +      SecurityContext.NONE;
    +
    +    return ctx;
       }
     
       override getMappedPropName(propName: string): string {
    
  • packages/compiler/src/schema/dom_security_schema.ts+100 119 modified
    @@ -20,109 +20,82 @@ import {SecurityContext} from '../core';
     
     /** Map from tagName|propertyName to SecurityContext. Properties applying to all tags use '*'. */
     let _SECURITY_SCHEMA!: {[k: string]: SecurityContext};
    +const SVG_NAMESPACE = 'svg';
    +const MATH_ML_NAMESPACE = 'math';
     
     export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} {
       if (!_SECURITY_SCHEMA) {
         _SECURITY_SCHEMA = {};
         // Case is insignificant below, all element and attribute names are lower-cased for lookup.
     
    -    registerContext(SecurityContext.HTML, ['iframe|srcdoc', '*|innerHTML', '*|outerHTML']);
    -    registerContext(SecurityContext.STYLE, ['*|style']);
    +    registerContext(SecurityContext.HTML, /** Namespace */ undefined, [
    +      ['iframe', ['srcdoc']],
    +      ['*', ['innerHTML', 'outerHTML']],
    +    ]);
    +    registerContext(SecurityContext.STYLE, /** Namespace */ undefined, [['*', ['style']]]);
         // NB: no SCRIPT contexts here, they are never allowed due to the parser stripping them.
    -    registerContext(SecurityContext.URL, [
    -      '*|formAction',
    -      'area|href',
    -      'a|href',
    -      'a|xlink:href',
    -      'form|action',
    +    registerContext(SecurityContext.URL, /** Namespace */ undefined, [
    +      ['*', ['formAction']],
    +      ['area', ['href']],
    +      ['a', ['href', 'xlink:href']],
    +      ['form', ['action']],
    +
    +      // The below two items are safe and should be removed but they require a G3 clean-up as a small number of tests fail.
    +      ['img', ['src']],
    +      ['video', ['src']],
    +    ]);
     
    +    registerContext(SecurityContext.URL, MATH_ML_NAMESPACE, [
           // 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',
    +      ['annotation', ['href', 'xlink:href']],
    +      ['annotation-xml', ['href', 'xlink:href']],
    +      ['maction', ['href', 'xlink:href']],
    +      ['malignmark', ['href', 'xlink:href']],
    +      ['math', ['href', 'xlink:href']],
    +      ['mroot', ['href', 'xlink:href']],
    +      ['msqrt', ['href', 'xlink:href']],
    +      ['merror', ['href', 'xlink:href']],
    +      ['mfrac', ['href', 'xlink:href']],
    +      ['mglyph', ['href', 'xlink:href']],
    +      ['msub', ['href', 'xlink:href']],
    +      ['msup', ['href', 'xlink:href']],
    +      ['msubsup', ['href', 'xlink:href']],
    +      ['mmultiscripts', ['href', 'xlink:href']],
    +      ['mprescripts', ['href', 'xlink:href']],
    +      ['mi', ['href', 'xlink:href']],
    +      ['mn', ['href', 'xlink:href']],
    +      ['mo', ['href', 'xlink:href']],
    +      ['mpadded', ['href', 'xlink:href']],
    +      ['mphantom', ['href', 'xlink:href']],
    +      ['mrow', ['href', 'xlink:href']],
    +      ['ms', ['href', 'xlink:href']],
    +      ['mspace', ['href', 'xlink:href']],
    +      ['mstyle', ['href', 'xlink:href']],
    +      ['mtable', ['href', 'xlink:href']],
    +      ['mtd', ['href', 'xlink:href']],
    +      ['mtr', ['href', 'xlink:href']],
    +      ['mtext', ['href', 'xlink:href']],
    +      ['mover', ['href', 'xlink:href']],
    +      ['munder', ['href', 'xlink:href']],
    +      ['munderover', ['href', 'xlink:href']],
    +      ['semantics', ['href', 'xlink:href']],
    +      ['none', ['href', 'xlink:href']],
    +    ]);
     
    -      // The below two items are safe and should be removed but they require a G3 clean-up as a small number of tests fail.
    -      'img|src',
    -      'video|src',
    +    registerContext(SecurityContext.RESOURCE_URL, /** Namespace */ undefined, [
    +      ['base', ['href']],
    +      ['embed', ['src']],
    +      ['frame', ['src']],
    +      ['iframe', ['src']],
    +      ['link', ['href']],
    +      ['object', ['codebase', 'data']],
         ]);
     
    -    registerContext(SecurityContext.RESOURCE_URL, [
    -      'base|href',
    -      'embed|src',
    -      'frame|src',
    -      'iframe|src',
    -      'link|href',
    -      'object|codebase',
    -      'object|data',
    -      'script|src',
    -      // The below two are for Script SVG
    -      // See: https://developer.mozilla.org/en-US/docs/Web/API/SVGScriptElement/href
    -      'script|href',
    -      'script|xlink:href',
    +    // The below are for Script SVG
    +    // See: https://developer.mozilla.org/en-US/docs/Web/API/SVGScriptElement/href
    +    registerContext(SecurityContext.RESOURCE_URL, SVG_NAMESPACE, [
    +      ['script', ['src', 'href', 'xlink:href']],
         ]);
     
         // Keep this in sync with SECURITY_SENSITIVE_ELEMENTS in packages/core/src/sanitization/sanitization.ts
    @@ -133,40 +106,48 @@ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} {
         // against the set that requires sanitization.
         // These are unsafe as `attributeName` can be `href` or `xlink:href`
         // See: http://b/463880509#comment7
    -    registerContext(SecurityContext.ATTRIBUTE_NO_BINDING, [
    -      'animate|attributeName',
    -      'animate|values',
    -      'animate|to',
    -      'animate|from',
    -      'set|to',
    -      'set|attributeName',
    -      'animateMotion|attributeName',
    -      'animateTransform|attributeName',
    -
    -      'unknown|attributeName',
    -      'unknown|values',
    -      'unknown|to',
    -      'unknown|from',
    -
    -      'iframe|sandbox',
    -      'iframe|allow',
    -      'iframe|allowFullscreen',
    -      'iframe|referrerPolicy',
    -      'iframe|csp',
    -      'iframe|fetchPriority',
    +    registerContext(SecurityContext.ATTRIBUTE_NO_BINDING, SVG_NAMESPACE, [
    +      ['animate', ['attributeName', 'values', 'to', 'from']],
    +      ['set', ['to', 'attributeName']],
    +      ['animateMotion', ['attributeName']],
    +      ['animateTransform', ['attributeName']],
    +    ]);
     
    -      'unknown|sandbox',
    -      'unknown|allow',
    -      'unknown|allowFullscreen',
    -      'unknown|referrerPolicy',
    -      'unknown|csp',
    -      'unknown|fetchPriority',
    +    registerContext(SecurityContext.ATTRIBUTE_NO_BINDING, /** Namespace */ undefined, [
    +      [
    +        'unknown',
    +        [
    +          'attributeName',
    +          'values',
    +          'to',
    +          'from',
    +          'sandbox',
    +          'allow',
    +          'allowFullscreen',
    +          'referrerPolicy',
    +          'csp',
    +          'fetchPriority',
    +        ],
    +      ],
    +      ['iframe', ['sandbox', 'allow', 'allowFullscreen', 'referrerPolicy', 'csp', 'fetchPriority']],
         ]);
       }
     
       return _SECURITY_SCHEMA;
     }
     
    -function registerContext(ctx: SecurityContext, specs: string[]) {
    -  for (const spec of specs) _SECURITY_SCHEMA[spec.toLowerCase()] = ctx;
    +function registerContext(
    +  ctx: SecurityContext,
    +  namespace: string | undefined,
    +  specs: readonly [tagName: string, attributeNames: readonly string[]][],
    +): void {
    +  for (const [element, attributeNames] of specs) {
    +    let tagName =
    +      namespace && element !== '*' && element !== 'unknown' ? `:${namespace}:${element}` : element;
    +    tagName = tagName.toLowerCase();
    +
    +    for (const attr of attributeNames) {
    +      _SECURITY_SCHEMA[`${tagName}|${attr.toLowerCase()}`] = ctx;
    +    }
    +  }
     }
    
  • packages/compiler/src/template_parser/binding_parser.ts+32 9 modified
    @@ -31,12 +31,13 @@ import {
       VariableBinding,
     } from '../expression_parser/ast';
     import {Parser} from '../expression_parser/parser';
    -import {mergeNsAndName} from '../ml_parser/tags';
    +import {mergeNsAndName, splitNsName} from '../ml_parser/tags';
     import {InterpolatedAttributeToken, InterpolatedTextToken} from '../ml_parser/tokens';
     import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
     import {ElementSchemaRegistry} from '../schema/element_schema_registry';
     import {CssSelector} from '../directive_matching';
     import {splitAtColon, splitAtPeriod} from '../util';
    +import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from '../template/pipeline/src/namespaces';
     
     const PROPERTY_PARTS_SEPARATOR = '.';
     const ATTRIBUTE_PREFIX = 'attr';
    @@ -878,20 +879,42 @@ export function calcPossibleSecurityContexts(
       isAttribute: boolean,
     ): SecurityContext[] {
       let ctxs: SecurityContext[];
    -  const nameToContext = (elName: string) => registry.securityContext(elName, propName, isAttribute);
    -
    -  if (selector === null) {
    -    ctxs = registry.allKnownElementNames().map(nameToContext);
    +  const [namespaceKey, baseSelector] = selector ? splitNsName(selector, false) : [null, selector];
    +  const nameToContext = (elName: string) => {
    +    const [nsStr, name] = splitNsName(elName, false);
    +    const ns = nsStr ?? namespaceKey;
    +    const fullName = ns ? `:${ns}:${name}` : name;
    +    return registry.securityContext(fullName, propName, isAttribute);
    +  };
    +
    +  const allKnownElements = registry.allKnownElementNames();
    +  if (baseSelector === null) {
    +    ctxs = allKnownElements.map(nameToContext);
       } else {
         ctxs = [];
    -    CssSelector.parse(selector).forEach((selector) => {
    -      const elementNames = selector.element ? [selector.element] : registry.allKnownElementNames();
    +    CssSelector.parse(baseSelector).forEach((selector) => {
    +      let elementNames = selector.element ? [selector.element] : allKnownElements;
    +      if (selector.element && !registry.hasElement(selector.element, [])) {
    +        const svgElement = `:${SVG_NAMESPACE}:${selector.element}`;
    +        const mathElement = `:${MATH_ML_NAMESPACE}:${selector.element}`;
    +        if (registry.hasElement(svgElement, [])) {
    +          elementNames = [svgElement];
    +        } else if (registry.hasElement(mathElement, [])) {
    +          elementNames = [mathElement];
    +        }
    +      }
           const notElementNames = new Set(
             selector.notSelectors
               .filter((selector) => selector.isElementSelector())
    -          .map((selector) => selector.element),
    +          .map((selector) => selector.element?.toLowerCase()),
           );
    -      const possibleElementNames = elementNames.filter((elName) => !notElementNames.has(elName));
    +      const possibleElementNames = elementNames.filter((elName) => {
    +        const elNameLowerCase = elName.toLowerCase();
    +        return (
    +          !notElementNames.has(elNameLowerCase) &&
    +          !notElementNames.has(splitNsName(elNameLowerCase)[1])
    +        );
    +      });
     
           ctxs.push(...possibleElementNames.map(nameToContext));
         });
    
  • packages/compiler/src/template/pipeline/src/ingest.ts+19 1 modified
    @@ -29,6 +29,7 @@ import {
       type ViewCompilationUnit,
     } from './compilation';
     import {BINARY_OPERATORS, namespaceForKey, prefixWithNamespace} from './conversion';
    +import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
     
     // Schema containing DOM elements and their properties.
     const domSchema = new DomElementSchemaRegistry();
    @@ -1328,7 +1329,24 @@ function ingestElementBindings(
     
       for (const attr of element.attributes) {
         // Attribute literal bindings, such as `attr.foo="bar"`.
    -    const securityContext = domSchema.securityContext(element.name, attr.name, true);
    +    const [ns, elementName] = splitNsName(element.name);
    +    let namespace = ns;
    +    if (!ns) {
    +      switch (op.namespace) {
    +        case ir.Namespace.SVG:
    +          namespace = SVG_NAMESPACE;
    +          break;
    +        case ir.Namespace.Math:
    +          namespace = MATH_ML_NAMESPACE;
    +          break;
    +      }
    +    }
    +
    +    const securityContext = domSchema.securityContext(
    +      namespace ? `:${namespace}:${elementName}` : elementName,
    +      attr.name,
    +      true,
    +    );
         bindings.push(
           ir.createBindingOp(
             op.xref,
    
  • packages/compiler/src/template/pipeline/src/namespaces.ts+10 0 added
    @@ -0,0 +1,10 @@
    +/**
    + * @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
    + */
    +
    +export const SVG_NAMESPACE = 'svg';
    +export const MATH_ML_NAMESPACE = 'math';
    
  • packages/compiler/test/schema/dom_element_schema_registry_spec.ts+6 4 modified
    @@ -156,16 +156,18 @@ If 'onAnything' is a directive input, make sure the directive is imported by the
         expect(registry.securityContext('base', 'href', false)).toBe(SecurityContext.RESOURCE_URL);
     
         // SVG animate and set attributes
    -    expect(registry.securityContext('animate', 'to', false)).toBe(
    +    expect(registry.securityContext(':svg:animate', 'to', false)).toBe(
           SecurityContext.ATTRIBUTE_NO_BINDING,
         );
    -    expect(registry.securityContext('animate', 'from', false)).toBe(
    +    expect(registry.securityContext(':svg:animate', 'from', false)).toBe(
           SecurityContext.ATTRIBUTE_NO_BINDING,
         );
    -    expect(registry.securityContext('animate', 'values', false)).toBe(
    +    expect(registry.securityContext(':svg:animate', 'values', false)).toBe(
    +      SecurityContext.ATTRIBUTE_NO_BINDING,
    +    );
    +    expect(registry.securityContext(':svg:set', 'to', false)).toBe(
           SecurityContext.ATTRIBUTE_NO_BINDING,
         );
    -    expect(registry.securityContext('set', 'to', false)).toBe(SecurityContext.ATTRIBUTE_NO_BINDING);
       });
     
       it('should detect properties on namespaced elements', () => {
    
  • packages/core/src/render3/interfaces/node.ts+5 0 modified
    @@ -416,6 +416,11 @@ export interface TNode {
        */
       value: any;
     
    +  /**
    +   * The namespace associated with this node.
    +   */
    +  namespace: string | null;
    +
       /**
        * Attributes associated with an element. We need to store attributes to support various
        * use-cases (attribute injection, content projection with selectors, directives matching).
    
  • packages/core/src/render3/tnode_manipulation.ts+2 0 modified
    @@ -26,6 +26,7 @@ import {assertPureTNodeType} from './node_assert';
     import {
       getCurrentParentTNode,
       getCurrentTNodePlaceholderOk,
    +  getNamespace,
       isCurrentTNodeParent,
       isInI18nBlock,
       isInSkipHydrationBlock,
    @@ -304,6 +305,7 @@ export function createTNode(
         flags,
         providerIndexes: 0,
         value: value,
    +    namespace: getNamespace(),
         attrs: attrs,
         mergedAttrs: null,
         localNames: null,
    
  • packages/core/src/sanitization/sanitization.ts+37 26 modified
    @@ -220,6 +220,7 @@ const RESOURCE_MAP: Record<string, Record<string, true | undefined> | undefined>
       'iframe': {'src': true},
       'media': {'src': true},
       'script': {'src': true, 'href': true, 'xlink:href': true},
    +  ':svg:script': {'src': true, 'href': true, 'xlink:href': true},
       'base': {'href': true},
       'link': {'href': true},
       'object': {'data': true, 'codebase': true},
    @@ -294,15 +295,15 @@ export const SECURITY_SENSITIVE_ELEMENTS: Record<
         'csp': true,
         'fetchpriority': true,
       },
    -  'animate': {
    +  ':svg:animate': {
         'attributename': true,
         'to': SECURITY_SENSITIVE_ATTRIBUTE_NAMES,
         'values': SECURITY_SENSITIVE_ATTRIBUTE_NAMES,
         'from': SECURITY_SENSITIVE_ATTRIBUTE_NAMES,
       },
    -  'set': {'attributename': true, 'to': SECURITY_SENSITIVE_ATTRIBUTE_NAMES},
    -  'animatemotion': {'attributename': true},
    -  'animatetransform': {'attributename': true},
    +  ':svg:set': {'attributename': true, 'to': SECURITY_SENSITIVE_ATTRIBUTE_NAMES},
    +  ':svg:animatemotion': {'attributename': true},
    +  ':svg:animatetransform': {'attributename': true},
     };
     
     /**
    @@ -315,37 +316,47 @@ export const SECURITY_SENSITIVE_ELEMENTS: Record<
     export function ɵɵvalidateAttribute<T = any>(value: T, tagName: string, attributeName: string): T {
       const lowerCaseTagName = tagName.toLowerCase();
       const lowerCaseAttrName = attributeName.toLowerCase();
    -  const validationConfig = SECURITY_SENSITIVE_ELEMENTS[lowerCaseTagName]?.[lowerCaseAttrName];
    -  if (!validationConfig) {
    -    return value;
    -  }
     
    -  const tNode = getSelectedTNode()!;
    -  if (tNode.type !== TNodeType.Element) {
    +  // Leverage tNode.namespace if active, otherwise check both namespaced and base variants.
    +  const tNode = getSelectedTNode();
    +  const fullTagName =
    +    lowerCaseTagName[0] !== ':' && tNode?.namespace
    +      ? `:${tNode.namespace}:${lowerCaseTagName}`
    +      : lowerCaseTagName;
    +
    +  const validationConfig = SECURITY_SENSITIVE_ELEMENTS[fullTagName]?.[lowerCaseAttrName];
    +
    +  if (!validationConfig) {
         return value;
       }
     
       const lView = getLView();
       if (lowerCaseTagName === 'iframe') {
    -    const element = getNativeByTNode(tNode, lView) as RElement;
    -    enforceIframeSecurity(element as HTMLIFrameElement);
    +    if (tNode?.type === TNodeType.Element) {
    +      const element = getNativeByTNode(tNode, lView) as RElement;
    +      enforceIframeSecurity(element as HTMLIFrameElement);
    +    }
       }
     
    +  const displayTagName = tagName[0] === ':' ? tagName.split(':').pop()! : tagName;
    +
       if (typeof validationConfig !== 'boolean') {
    -    const element = getNativeByTNode(tNode, lView) as SVGAnimateElement;
    -    const attributeNameValue = element.getAttribute('attributeName');
    +    if (tNode?.type === TNodeType.Element) {
    +      const element = getNativeByTNode(tNode, lView) as SVGAnimateElement;
    +      const attributeNameValue = element.getAttribute('attributeName');
     
    -    if (attributeNameValue && validationConfig.has(attributeNameValue.toLowerCase())) {
    -      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 when the "attributeName" is set to \'${attributeNameValue}\'. \n` +
    -          `To fix this, switch the \`${attributeNameValue}\` binding to a static attribute ` +
    -          `in a template or in host bindings section.`;
    +      if (attributeNameValue && validationConfig.has(attributeNameValue.toLowerCase())) {
    +        const errorMessage =
    +          ngDevMode &&
    +          `Angular has detected that the \`${attributeName}\` was applied ` +
    +            `as a binding to the <${displayTagName}> element${getTemplateLocationDetails(lView)}. ` +
    +            `For security reasons, the \`${attributeName}\` can be set on the <${displayTagName}> element ` +
    +            `as a static attribute only when the "attributeName" is set to \'${attributeNameValue}\'. \n` +
    +            `To fix this, switch the \`${attributeNameValue}\` binding to a static attribute ` +
    +            `in a template or in host bindings section.`;
     
    -      throw new RuntimeError(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING, errorMessage);
    +        throw new RuntimeError(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING, errorMessage);
    +      }
         }
     
         return value;
    @@ -354,8 +365,8 @@ export function ɵɵvalidateAttribute<T = any>(value: T, tagName: string, attrib
       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 binding to the <${displayTagName}> element${getTemplateLocationDetails(lView)}. ` +
    +      `For security reasons, the \`${attributeName}\` can be set on the <${displayTagName}> 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.`;
    
  • packages/core/test/bundling/create_component/bundle.golden_symbols.json+1 0 modified
    @@ -422,6 +422,7 @@
           "getInsertInFrontOfRNodeWithNoI18n",
           "getLView",
           "getLViewParent",
    +      "getNamespace",
           "getNativeByIndex",
           "getNativeByTNode",
           "getNearestLContainer",
    
  • packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json+1 0 modified
    @@ -388,6 +388,7 @@
           "getInsertInFrontOfRNodeWithNoI18n",
           "getLView",
           "getLViewParent",
    +      "getNamespace",
           "getNativeByTNode",
           "getNearestLContainer",
           "getNextLContainer",
    
  • packages/core/test/render3/is_shape_of.ts+1 0 modified
    @@ -160,6 +160,7 @@ const ShapeOfTNode: ShapeOf<TNode> = {
       flags: true,
       providerIndexes: true,
       value: true,
    +  namespace: true,
       attrs: true,
       mergedAttrs: true,
       localNames: true,
    
  • packages/core/test/sanitization/sanitization_spec.ts+3 1 modified
    @@ -125,7 +125,9 @@ describe('sanitization', () => {
             contextsByProp.set(prop, contexts);
             // check only in case a prop can be a part of both URL contexts
             if (contexts.size === 2) {
    -          expect(getUrlSanitizer(tag, prop)).toEqual(sanitizerNameByContext.get(context)!);
    +          expect(getUrlSanitizer(tag, prop))
    +            .withContext(`key: ${key}, context: ${context}`)
    +            .toEqual(sanitizerNameByContext.get(context)!);
             }
           }
         });
    
0011664d1c5c

fix(core): reject script element as a dynamic component host

https://github.com/angular/angularAlan AgiusMay 13, 2026via body-scan-shorthand
2 files changed · +57 0
  • packages/core/src/render3/instructions/shared.ts+7 0 modified
    @@ -21,6 +21,7 @@ import {stringify} from '../../util/stringify';
     import {assertFirstCreatePass, assertHasParent, assertLView} from '../assert';
     import {attachPatchData} from '../context_discovery';
     import {getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di';
    +import {RuntimeError, RuntimeErrorCode} from '../../errors';
     import {throwMultipleComponentError} from '../errors';
     import {ComponentDef, ComponentTemplate, DirectiveDef, RenderFlags} from '../interfaces/definition';
     import {
    @@ -180,6 +181,12 @@ export function locateHostElement(
         encapsulation === ViewEncapsulation.ShadowDom ||
         encapsulation === ViewEncapsulation.ExperimentalIsolatedShadowDom;
       const rootElement = renderer.selectRootElement(elementOrSelector, preserveContent);
    +  if (rootElement.tagName.toLowerCase() === 'script') {
    +    throw new RuntimeError(
    +      RuntimeErrorCode.UNSAFE_VALUE_IN_SCRIPT,
    +      ngDevMode && `"<script>" tag is not allowed as a component host element.`,
    +    );
    +  }
       applyRootElementTransform(rootElement as HTMLElement);
       return rootElement;
     }
    
  • packages/core/test/acceptance/security_spec.ts+50 0 modified
    @@ -10,7 +10,9 @@ import {NgIf} from '@angular/common';
     import {DomSanitizer} from '@angular/platform-browser';
     import {
       Component,
    +  createComponent,
       Directive,
    +  EnvironmentInjector,
       inject,
       provideZoneChangeDetection,
       TemplateRef,
    @@ -852,3 +854,51 @@ describe('innerHTML processing', () => {
         expect(fixture.nativeElement.innerHTML).not.toContain('action');
       });
     });
    +
    +describe('Component host element validation', () => {
    +  it('should throw an error when dynamically mounting a component onto a script tag', () => {
    +    @Component({
    +      selector: 'my-sink',
    +      template: '',
    +    })
    +    class MySink {}
    +
    +    const scriptHost = document.createElement('script');
    +    document.head.appendChild(scriptHost);
    +
    +    try {
    +      const environmentInjector = TestBed.inject(EnvironmentInjector);
    +      expect(() => {
    +        createComponent(MySink, {
    +          environmentInjector,
    +          hostElement: scriptHost,
    +        });
    +      }).toThrowError(/"<script>" tag is not allowed as a component host element/);
    +    } finally {
    +      scriptHost.remove();
    +    }
    +  });
    +
    +  it('should throw an error when dynamically mounting a component onto an SVG script tag', () => {
    +    @Component({
    +      selector: 'my-svg-sink',
    +      template: '',
    +    })
    +    class MySvgSink {}
    +
    +    const svgScriptHost = document.createElementNS('http://www.w3.org/2000/svg', 'script');
    +    document.head.appendChild(svgScriptHost);
    +
    +    try {
    +      const environmentInjector = TestBed.inject(EnvironmentInjector);
    +      expect(() => {
    +        createComponent(MySvgSink, {
    +          environmentInjector,
    +          hostElement: svgScriptHost,
    +        });
    +      }).toThrowError(/"<script>" tag is not allowed as a component host element/);
    +    } finally {
    +      svgScriptHost.remove();
    +    }
    +  });
    +});
    

Vulnerability mechanics

Root cause

"Missing validation in `locateHostElement` allowed dynamic components to be mounted on `<script>` or `<svg:script>` elements, bypassing script-execution restrictions."

Attack vector

An attacker who can control the host element or selector parameter passed to `createComponent` can supply a `<script>` or `<svg:script>` element as the mount target. Because the framework did not validate the host element's tag name, the dynamic component would be instantiated directly on the script tag, bypassing Angular's script-execution restrictions and allowing arbitrary JavaScript execution in the user's browser. This is a client-side Cross-Site Scripting (XSS) attack that requires the application to pass user-controlled input to `createComponent` without additional sanitization.

Affected code

The vulnerability resides in `packages/core/src/render3/instructions/shared.ts` in the `locateHostElement` function, which did not reject `<script>` or `<svg:script>` elements when used as a dynamic component host. The patch also updates the security schema in `packages/compiler/src/schema/dom_security_schema.ts` and the sanitization logic in `packages/core/src/sanitization/sanitization.ts` to properly namespace SVG script elements.

What the fix does

Patch [patch_id=6084663] adds a guard in `locateHostElement` that checks whether the resolved host element's `tagName` is `'script'` and throws a `RuntimeError` with code `UNSAFE_VALUE_IN_SCRIPT` if so, preventing dynamic component mounting on both HTML `<script>` and SVG `<script>` elements. Patch [patch_id=6084662] refactors the security schema and sanitization maps to use namespaced keys (e.g. `:svg:script`) so that SVG script elements are correctly recognized as security-sensitive and their resource URLs are properly sanitized. Together these changes close the bypass by rejecting script-element hosts at runtime and ensuring the security schema correctly identifies namespaced script elements.

Preconditions

  • inputThe application must accept user-controlled inputs that are passed as a selector/host element to `createComponent`.
  • configThe application does not perform separate input sanitization before feeding values to the dynamic creation APIs.

Generated on Jun 15, 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.