mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, Record<string, true | undefined> | 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<string> = 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<Record<string, ReadonlySet<string>>> = {
|
||||
'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<string> = new Set(['href', 'xlink:href']);
|
||||
|
||||
export const SECURITY_SENSITIVE_ELEMENTS: Record<
|
||||
string,
|
||||
Record<string, true | undefined | ReadonlySet<string>> | 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<Record<string, ReadonlySet<st
|
||||
export function ɵɵvalidateAttribute<T = any>(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.`;
|
||||
|
||||
@@ -158,6 +158,7 @@ const ShapeOfTNode: ShapeOf<TNode> = {
|
||||
flags: true,
|
||||
providerIndexes: true,
|
||||
value: true,
|
||||
namespace: true,
|
||||
attrs: true,
|
||||
mergedAttrs: true,
|
||||
localNames: true,
|
||||
|
||||
@@ -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)!);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user