Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata, SecurityContex
import {isNgContainer, isNgContent, splitNsName} from '../ml_parser/tags';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from '../template/pipeline/src/namespaces';
import {dashCaseToCamelCase} from '../util';
import {checkSecurityContext, SECURITY_SCHEMA} from './dom_security_schema';
import {checkSecurityContext} from './dom_security_schema';
import {ElementSchemaRegistry} from './element_schema_registry';

const BOOLEAN = 'boolean';
Expand Down
197 changes: 105 additions & 92 deletions packages/compiler/src/schema/dom_security_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,89 +36,96 @@ export enum SecurityContext {
// =================================================================================================

/**
* Map from tagName|propertyName to SecurityContext. Properties applying to all tags use '*'.
* Map from property name to namespace and tag name to SecurityContext.
* Properties applying to all tags use '*'.
* Properties applying to all namespaces use ''.
*/
let _SECURITY_SCHEMA!: {[k: string]: SecurityContext};
let _SECURITY_SCHEMA!: Record<string, Record<string, Record<string, SecurityContext>>>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to create a local type that expressing the security schema so that we can better document what it looks like a bit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call

const SVG_NAMESPACE = 'svg';
const MATH_ML_NAMESPACE = 'math';
const NO_NAMESPACE = '';
const MATCH_ALL_ELEMENTS = '*';

/**
* @remarks Keep is a copy of DOM Security Schema.
* @see [SECURITY_SCHEMA](../../../compiler/src/schema/dom_security_schema.ts)
*/
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, /** 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, /** 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
['*', ['href', 'xlink:href']],
]);

registerContext(SecurityContext.RESOURCE_URL, /** Namespace */ undefined, [
['base', ['href']],
['embed', ['src']],
['frame', ['src']],
['iframe', ['src']],
['link', ['href']],
['object', ['codebase', 'data']],
]);

registerContext(SecurityContext.URL, SVG_NAMESPACE, [['a', ['href', 'xlink:href']]]);

// Keep this in sync with SECURITY_SENSITIVE_ELEMENTS in packages/core/src/sanitization/sanitization.ts
// The `unknown` elements refer to cases when we need to validate the input/binding in a directive (host bindings)
// and the directive can be applied to multiple different elements (with different tag names). In this case we generate
// a special instruction that an attribute might potentially be security-sensitive and defer the actual security check
// to runtime, when we apply that directive to a concrete elements, thus we can check the combination of tag+attribute
// 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, SVG_NAMESPACE, [
['animate', ['attributeName', 'values', 'to', 'from']],
['set', ['to', 'attributeName']],
['animateMotion', ['attributeName']],
['animateTransform', ['attributeName']],
]);

registerContext(SecurityContext.ATTRIBUTE_NO_BINDING, /** Namespace */ undefined, [
export function SECURITY_SCHEMA(): Record<string, Record<string, Record<string, SecurityContext>>> {
if (_SECURITY_SCHEMA) {
return _SECURITY_SCHEMA;
}

_SECURITY_SCHEMA = {};

// Case is insignificant below, all element and attribute names are lower-cased for lookup.

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, /** 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
['*', ['href', 'xlink:href']],
]);

registerContext(SecurityContext.RESOURCE_URL, /** Namespace */ undefined, [
['base', ['href']],
['embed', ['src']],
['frame', ['src']],
['iframe', ['src']],
['link', ['href']],
['object', ['codebase', 'data']],
]);

registerContext(SecurityContext.URL, SVG_NAMESPACE, [['a', ['href', 'xlink:href']]]);

// Keep this in sync with SECURITY_SENSITIVE_ELEMENTS in packages/core/src/sanitization/sanitization.ts
// The `unknown` elements refer to cases when we need to validate the input/binding in a directive (host bindings)
// and the directive can be applied to multiple different elements (with different tag names). In this case we generate
// a special instruction that an attribute might potentially be security-sensitive and defer the actual security check
// to runtime, when we apply that directive to a concrete elements, thus we can check the combination of tag+attribute
// 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, SVG_NAMESPACE, [
['animate', ['attributeName', 'values', 'to', 'from']],
['set', ['to', 'attributeName']],
['animateMotion', ['attributeName']],
['animateTransform', ['attributeName']],
]);

registerContext(SecurityContext.ATTRIBUTE_NO_BINDING, /** Namespace */ undefined, [
[
'unknown',
[
'unknown',
[
'attributeName',
'values',
'to',
'from',
'sandbox',
'allow',
'allowFullscreen',
'referrerPolicy',
'csp',
'fetchPriority',
],
'attributeName',
'values',
'to',
'from',
'sandbox',
'allow',
'allowFullscreen',
'referrerPolicy',
'csp',
'fetchPriority',
],
['iframe', ['sandbox', 'allow', 'allowFullscreen', 'referrerPolicy', 'csp', 'fetchPriority']],
]);
}
],
['iframe', ['sandbox', 'allow', 'allowFullscreen', 'referrerPolicy', 'csp', 'fetchPriority']],
]);

return _SECURITY_SCHEMA;
}
Expand All @@ -128,15 +135,15 @@ function registerContext(
namespace: string | undefined,
specs: readonly [tagName: string, attributeNames: readonly string[]][],
): void {
const nsKey = namespace ?? NO_NAMESPACE;
for (const [element, attributeNames] of specs) {
let tagName = element;
if (namespace && element !== 'unknown') {
tagName = `:${namespace}:${element}`;
}
tagName = tagName.toLowerCase();
const tagName = element.toLowerCase();

for (const attr of attributeNames) {
_SECURITY_SCHEMA[`${tagName}|${attr.toLowerCase()}`] = ctx;
const attrLower = attr.toLowerCase();
const attrSchema = (_SECURITY_SCHEMA[attrLower] ??= {});
const nsSchema = (attrSchema[nsKey] ??= {});
nsSchema[tagName] = ctx;
}
}
}
Expand All @@ -153,21 +160,27 @@ export function checkSecurityContext(
namespace?: string | null,
): SecurityContext {
const securitySchema = SECURITY_SCHEMA();
propName = propName.toLowerCase();
tagName = tagName.toLowerCase();
const attrSchema = securitySchema[propName.toLowerCase()];
if (!attrSchema) {
return SecurityContext.NONE;
}

let namespacedTag = tagName;
let nsWildcardTag: string | undefined;
const tagLower = tagName.toLowerCase();
let context: SecurityContext | undefined;

if (namespace === SVG_NAMESPACE || namespace === MATH_ML_NAMESPACE) {
namespacedTag = `:${namespace}:${tagName}`;
nsWildcardTag = `:${namespace}:*`;
if (namespace) {
const nsSchema = attrSchema[namespace];
if (nsSchema) {
context = nsSchema[tagLower] ?? nsSchema[MATCH_ALL_ELEMENTS];
}
}

if (context === undefined) {
const defaultSchema = attrSchema[NO_NAMESPACE];
if (defaultSchema) {
context = defaultSchema[tagLower] ?? defaultSchema[MATCH_ALL_ELEMENTS];
}
}

return (
securitySchema[namespacedTag + '|' + propName] ??
(nsWildcardTag !== undefined ? securitySchema[nsWildcardTag + '|' + propName] : undefined) ??
securitySchema['*|' + propName] ??
SecurityContext.NONE
);
return context ?? SecurityContext.NONE;
}
2 changes: 1 addition & 1 deletion packages/core/src/render3/i18n/i18n_parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1003,7 +1003,7 @@ function splitNsName(elementName: string, fatal: boolean = true): [string | null
}

function i18nResolveSanitizer(attrName: string, tagName?: string): SanitizerFn | null {
let schemaContext = SecurityContext.NONE;
let schemaContext: SecurityContext;

if (tagName) {
const [ns, name] = splitNsName(tagName, false);
Expand Down
Loading
Loading