From 8a814aed96076354891c3413c7ddb7e1cb69f4c4 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 18 May 2026 22:09:39 +0200 Subject: [PATCH] refactor(core): align namespaced attribute validation and security schema contexts Refactors the element security schema lookups and runtime attribute validation to consistently account for SVG and MathML namespaces. This improves the modularity and accuracy of security context mapping during template compilation and runtime constant evaluation, eliminating redundant or false-positive lifecycle checks. --- .../src/schema/dom_element_schema_registry.ts | 14 +- .../src/schema/dom_security_schema.ts | 234 ++++++++---------- .../src/template/pipeline/src/ingest.ts | 20 +- .../src/template/pipeline/src/namespaces.ts | 10 + .../src/template_parser/binding_parser.ts | 60 +++-- .../dom_element_schema_registry_spec.ts | 13 + packages/core/src/render3/interfaces/node.ts | 5 + .../core/src/render3/tnode_manipulation.ts | 2 + .../core/src/sanitization/sanitization.ts | 106 +++++--- packages/core/test/render3/is_shape_of.ts | 1 + .../test/sanitization/sanitization_spec.ts | 4 +- 11 files changed, 284 insertions(+), 185 deletions(-) create mode 100644 packages/compiler/src/template/pipeline/src/namespaces.ts diff --git a/packages/compiler/src/schema/dom_element_schema_registry.ts b/packages/compiler/src/schema/dom_element_schema_registry.ts index f6cd52c9cb6..0b3e5981437 100644 --- a/packages/compiler/src/schema/dom_element_schema_registry.ts +++ b/packages/compiler/src/schema/dom_element_schema_registry.ts @@ -397,12 +397,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 { diff --git a/packages/compiler/src/schema/dom_security_schema.ts b/packages/compiler/src/schema/dom_security_schema.ts index dfe5618dd53..5d3c635d2d0 100644 --- a/packages/compiler/src/schema/dom_security_schema.ts +++ b/packages/compiler/src/schema/dom_security_schema.ts @@ -20,158 +20,130 @@ 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', - 'area|ping', - 'audio|src', - 'a|href', - 'a|xlink:href', - 'a|ping', - 'blockquote|cite', - 'body|background', - 'del|cite', - 'form|action', - 'img|src', - 'input|src', - 'ins|cite', - 'q|cite', - 'source|src', - 'track|src', - 'video|poster', - 'video|src', + registerContext(SecurityContext.URL, /** Namespace */ undefined, [ + ['*', ['formAction']], + ['area', ['href']], + ['a', ['href', 'xlink:href']], + ['form', ['action']], - // 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', + // 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, [ - 'applet|code', - 'applet|codebase', - 'base|href', - 'embed|src', - 'frame|src', - 'head|profile', - 'html|manifest', - 'iframe|src', - 'link|href', - 'media|src', - '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', + 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', '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']], + ]); + + registerContext(SecurityContext.RESOURCE_URL, /** Namespace */ undefined, [ + ['base', ['href']], + ['embed', ['src']], + ['frame', ['src']], + ['iframe', ['src']], + ['link', ['href']], + ['object', ['codebase', 'data']], + ]); + + // 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 // 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, SVG_NAMESPACE, [ + ['animate', ['attributeName', 'values', 'to', 'from']], + ['set', ['to', 'attributeName']], + ['animateMotion', ['attributeName']], + ['animateTransform', ['attributeName']], + ]); - 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', + 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; + } + } } diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index eeca7e7c01f..c53d26b29a0 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -28,6 +28,7 @@ import { type ViewCompilationUnit, } from './compilation'; import {BINARY_OPERATORS, namespaceForKey, prefixWithNamespace} from './conversion'; +import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces'; const compatibilityMode = ir.CompatibilityMode.TemplateDefinitionBuilder; @@ -1262,7 +1263,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, diff --git a/packages/compiler/src/template/pipeline/src/namespaces.ts b/packages/compiler/src/template/pipeline/src/namespaces.ts new file mode 100644 index 00000000000..56ccf98f102 --- /dev/null +++ b/packages/compiler/src/template/pipeline/src/namespaces.ts @@ -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'; diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts index 136b36a579d..35e65b1a427 100644 --- a/packages/compiler/src/template_parser/binding_parser.ts +++ b/packages/compiler/src/template_parser/binding_parser.ts @@ -33,12 +33,13 @@ import { } from '../expression_parser/ast'; import {Parser} from '../expression_parser/parser'; import {InterpolationConfig} from '../ml_parser/defaults'; -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 '../selector'; import {splitAtColon, splitAtPeriod} from '../util'; +import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from '../template/pipeline/src/namespaces'; const PROPERTY_PARTS_SEPARATOR = '.'; const ATTRIBUTE_PREFIX = 'attr'; @@ -850,24 +851,47 @@ export function calcPossibleSecurityContexts( propName: string, isAttribute: boolean, ): SecurityContext[] { - const ctxs: SecurityContext[] = []; - CssSelector.parse(selector).forEach((selector) => { - const elementNames = selector.element ? [selector.element] : registry.allKnownElementNames(); - const notElementNames = new Set( - selector.notSelectors - .filter((selector) => selector.isElementSelector()) - .map((selector) => selector.element), - ); - const possibleElementNames = elementNames.filter( - (elementName) => !notElementNames.has(elementName), - ); + let ctxs: SecurityContext[]; + 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); + }; - ctxs.push( - ...possibleElementNames.map((elementName) => - registry.securityContext(elementName, propName, isAttribute), - ), - ); - }); + const allKnownElements = registry.allKnownElementNames(); + if (baseSelector === null) { + ctxs = allKnownElements.map(nameToContext); + } else { + ctxs = []; + 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?.toLowerCase()), + ); + const possibleElementNames = elementNames.filter((elName) => { + const elNameLowerCase = elName.toLowerCase(); + return ( + !notElementNames.has(elNameLowerCase) && + !notElementNames.has(splitNsName(elNameLowerCase)[1]) + ); + }); + + ctxs.push(...possibleElementNames.map(nameToContext)); + }); + } return ctxs.length === 0 ? [SecurityContext.NONE] : Array.from(new Set(ctxs)).sort(); } diff --git a/packages/compiler/test/schema/dom_element_schema_registry_spec.ts b/packages/compiler/test/schema/dom_element_schema_registry_spec.ts index 209b609ef6d..49fcca7a6d1 100644 --- a/packages/compiler/test/schema/dom_element_schema_registry_spec.ts +++ b/packages/compiler/test/schema/dom_element_schema_registry_spec.ts @@ -154,6 +154,19 @@ If 'onAnything' is a directive input, make sure the directive is imported by the expect(registry.securityContext('a', 'style', false)).toBe(SecurityContext.STYLE); expect(registry.securityContext('ins', 'cite', false)).toBe(SecurityContext.URL); expect(registry.securityContext('base', 'href', false)).toBe(SecurityContext.RESOURCE_URL); + // SVG animate and set attributes + expect(registry.securityContext(':svg:animate', 'to', false)).toBe( + SecurityContext.ATTRIBUTE_NO_BINDING, + ); + expect(registry.securityContext(':svg:animate', 'from', false)).toBe( + SecurityContext.ATTRIBUTE_NO_BINDING, + ); + expect(registry.securityContext(':svg:animate', 'values', false)).toBe( + SecurityContext.ATTRIBUTE_NO_BINDING, + ); + expect(registry.securityContext(':svg:set', 'to', false)).toBe( + SecurityContext.ATTRIBUTE_NO_BINDING, + ); }); it('should detect properties on namespaced elements', () => { diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index f789847806b..0c618db3b1d 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -368,6 +368,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). diff --git a/packages/core/src/render3/tnode_manipulation.ts b/packages/core/src/render3/tnode_manipulation.ts index cd2b881d433..6d55742138d 100644 --- a/packages/core/src/render3/tnode_manipulation.ts +++ b/packages/core/src/render3/tnode_manipulation.ts @@ -26,6 +26,7 @@ import {assertPureTNodeType} from './node_assert'; import { getCurrentParentTNode, getCurrentTNodePlaceholderOk, + getNamespace, isCurrentTNodeParent, isInI18nBlock, isInSkipHydrationBlock, @@ -288,6 +289,7 @@ export function createTNode( flags, providerIndexes: 0, value: value, + namespace: getNamespace(), attrs: attrs, mergedAttrs: null, localNames: null, diff --git a/packages/core/src/sanitization/sanitization.ts b/packages/core/src/sanitization/sanitization.ts index dc5f99ff382..dc8e293b1bc 100644 --- a/packages/core/src/sanitization/sanitization.ts +++ b/packages/core/src/sanitization/sanitization.ts @@ -214,8 +214,17 @@ export function ɵɵtrustConstantResourceUrl(url: TemplateStringsArray): Trusted } // Define sets outside the function for O(1) lookups and memory efficiency -const SRC_RESOURCE_TAGS = new Set(['embed', 'frame', 'iframe', 'media', 'script']); -const HREF_RESOURCE_TAGS = new Set(['base', 'link', 'script']); +const RESOURCE_MAP: Record | undefined> = { + 'embed': {'src': true}, + 'frame': {'src': true}, + '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}, +}; /** * Detects which sanitizer to use for URL property, based on tag name and prop name. @@ -225,10 +234,7 @@ const HREF_RESOURCE_TAGS = new Set(['base', 'link', 'script']); * If tag and prop names don't match Resource URL schema, use URL sanitizer. */ 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'); + const isResource = RESOURCE_MAP[tag.toLowerCase()]?.[prop.toLowerCase()] === true; return isResource ? ɵɵsanitizeResourceUrl : ɵɵsanitizeUrl; } @@ -285,19 +291,32 @@ const attributeName: ReadonlySet = new Set(['attributename']); * @remarks Keep this in sync with DOM Security Schema. * @see [SECURITY_SCHEMA](../../../compiler/src/schema/dom_security_schema.ts) */ -export const SECURITY_SENSITIVE_ELEMENTS: Readonly>> = { - 'iframe': new Set([ - 'sandbox', - 'allow', - 'allowfullscreen', - 'referrerpolicy', - 'csp', - 'fetchpriority', - ]), - 'animate': attributeName, - 'set': attributeName, - 'animatemotion': attributeName, - 'animatetransform': attributeName, +/** + * Set of attributes that are sensitive and should be sanitized. + */ +const SECURITY_SENSITIVE_ATTRIBUTE_NAMES: ReadonlySet = new Set(['href', 'xlink:href']); + +export const SECURITY_SENSITIVE_ELEMENTS: Record< + string, + Record> | undefined +> = { + 'iframe': { + 'sandbox': true, + 'allow': true, + 'allowfullscreen': true, + 'referrerpolicy': true, + 'csp': true, + 'fetchpriority': true, + }, + ':svg:animate': { + 'attributename': true, + 'to': SECURITY_SENSITIVE_ATTRIBUTE_NAMES, + 'values': SECURITY_SENSITIVE_ATTRIBUTE_NAMES, + 'from': SECURITY_SENSITIVE_ATTRIBUTE_NAMES, + }, + ':svg:set': {'attributename': true, 'to': SECURITY_SENSITIVE_ATTRIBUTE_NAMES}, + ':svg:animatemotion': {'attributename': true}, + ':svg:animatetransform': {'attributename': true}, }; /** @@ -310,26 +329,57 @@ export const SECURITY_SENSITIVE_ELEMENTS: Readonly(value: T, tagName: string, attributeName: string): T { 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) { + + // 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') { + 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 <${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); + } + } + + return value; + } 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.`; diff --git a/packages/core/test/render3/is_shape_of.ts b/packages/core/test/render3/is_shape_of.ts index f0207782f67..741ffb211f3 100644 --- a/packages/core/test/render3/is_shape_of.ts +++ b/packages/core/test/render3/is_shape_of.ts @@ -158,6 +158,7 @@ const ShapeOfTNode: ShapeOf = { flags: true, providerIndexes: true, value: true, + namespace: true, attrs: true, mergedAttrs: true, localNames: true, diff --git a/packages/core/test/sanitization/sanitization_spec.ts b/packages/core/test/sanitization/sanitization_spec.ts index bdfbc388266..5128ef284a0 100644 --- a/packages/core/test/sanitization/sanitization_spec.ts +++ b/packages/core/test/sanitization/sanitization_spec.ts @@ -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)!); } } });