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)!); } } });