feat(ivy): provide sanitization methods which can be tree shaken (#22540)

By providing a top level sanitization methods (rather than service) the
compiler can generate calls into the methods only when needed. This makes
the methods tree shakable.

PR Close #22540
This commit is contained in:
Miško Hevery
2018-03-01 17:14:01 -08:00
committed by Kara Erickson
parent 538f1d980f
commit 6d1367d297
15 changed files with 592 additions and 77 deletions

View File

@ -6,10 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {isDevMode} from '@angular/core';
import {isDevMode} from '../application_ref';
import {InertBodyHelper} from './inert_body';
import {sanitizeSrcset, sanitizeUrl} from './url_sanitizer';
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
function tagSet(tags: string): {[k: string]: boolean} {
const res: {[k: string]: boolean} = {};
@ -143,21 +142,17 @@ class SanitizingHtmlSerializer {
for (let i = 0; i < elAttrs.length; i++) {
const elAttr = elAttrs.item(i);
const attrName = elAttr.name;
let value = elAttr.value;
const lower = attrName.toLowerCase();
if (!VALID_ATTRS.hasOwnProperty(lower)) {
this.sanitizedSomething = true;
continue;
}
let value = elAttr.value;
// TODO(martinprobst): Special case image URIs for data:image/...
if (URI_ATTRS[lower]) value = sanitizeUrl(value);
if (URI_ATTRS[lower]) value = _sanitizeUrl(value);
if (SRCSET_ATTRS[lower]) value = sanitizeSrcset(value);
this.buf.push(' ');
this.buf.push(attrName);
this.buf.push('="');
this.buf.push(encodeEntities(value));
this.buf.push('"');
};
this.buf.push(' ', attrName, '="', encodeEntities(value), '"');
}
this.buf.push('>');
}
@ -173,7 +168,9 @@ class SanitizingHtmlSerializer {
private chars(chars: string) { this.buf.push(encodeEntities(chars)); }
checkClobberedElement(node: Node, nextNode: Node): Node {
if (nextNode && node.contains(nextNode)) {
if (nextNode &&
(node.compareDocumentPosition(nextNode) &
Node.DOCUMENT_POSITION_CONTAINED_BY) === Node.DOCUMENT_POSITION_CONTAINED_BY) {
throw new Error(
`Failed to sanitize html because the element is clobbered: ${(node as Element).outerHTML}`);
}
@ -214,7 +211,7 @@ let inertBodyHelper: InertBodyHelper;
* Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to
* the DOM in a browser environment.
*/
export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
let inertBodyElement: HTMLElement|null = null;
try {
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc);
@ -259,8 +256,8 @@ export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
}
function getTemplateContent(el: Node): Node|null {
return 'content' in el && isTemplateElement(el) ? (<any>el).content : null;
return 'content' in el && isTemplateElement(el) ? el.content : null;
}
function isTemplateElement(el: Node): boolean {
function isTemplateElement(el: Node): el is HTMLTemplateElement {
return el.nodeType === Node.ELEMENT_NODE && el.nodeName === 'TEMPLATE';
}

View File

@ -0,0 +1,237 @@
/**
* @license
* Copyright Google Inc. 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.io/license
*/
import {stringify} from '../render3/util';
import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer';
import {_sanitizeStyle as _sanitizeStyle} from './style_sanitizer';
import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer';
const BRAND = '__SANITIZER_TRUSTED_BRAND__';
/**
* A branded trusted string used with sanitization.
*
* See: {@link TrustedHtmlString}, {@link TrustedResourceUrlString}, {@link TrustedScriptString},
* {@link TrustedStyleString}, {@link TrustedUrlString}
*/
export interface TrustedString extends String {
'__SANITIZER_TRUSTED_BRAND__': 'Html'|'Style'|'Script'|'Url'|'ResourceUrl';
}
/**
* A branded trusted string used with sanitization of `html` strings.
*
* See: {@link bypassSanitizationTrustHtml} and {@link htmlSanitizer}.
*/
export interface TrustedHtmlString extends TrustedString { '__SANITIZER_TRUSTED_BRAND__': 'Html'; }
/**
* A branded trusted string used with sanitization of `style` strings.
*
* See: {@link bypassSanitizationTrustStyle} and {@link styleSanitizer}.
*/
export interface TrustedStyleString extends TrustedString {
'__SANITIZER_TRUSTED_BRAND__': 'Style';
}
/**
* A branded trusted string used with sanitization of `url` strings.
*
* See: {@link bypassSanitizationTrustScript} and {@link scriptSanitizer}.
*/
export interface TrustedScriptString extends TrustedString {
'__SANITIZER_TRUSTED_BRAND__': 'Script';
}
/**
* A branded trusted string used with sanitization of `url` strings.
*
* See: {@link bypassSanitizationTrustUrl} and {@link urlSanitizer}.
*/
export interface TrustedUrlString extends TrustedString { '__SANITIZER_TRUSTED_BRAND__': 'Url'; }
/**
* A branded trusted string used with sanitization of `resourceUrl` strings.
*
* See: {@link bypassSanitizationTrustResourceUrl} and {@link resourceUrlSanitizer}.
*/
export interface TrustedResourceUrlString extends TrustedString {
'__SANITIZER_TRUSTED_BRAND__': 'ResourceUrl';
}
/**
* An `html` sanitizer which converts untrusted `html` **string** into trusted string by removing
* dangerous content.
*
* This method parses the `html` and locates potentially dangerous content (such as urls and
* javascript) and removes it.
*
* It is possible to mark a string as trusted by calling {@link bypassSanitizationTrustHtml}.
*
* @param unsafeHtml untrusted `html`, typically from the user.
* @returns `html` string which is safe to display to user, because all of the dangerous javascript
* and urls have been removed.
*/
export function sanitizeHtml(unsafeHtml: any): string {
if (unsafeHtml instanceof String && (unsafeHtml as TrustedHtmlString)[BRAND] === 'Html') {
return unsafeHtml.toString();
}
return _sanitizeHtml(document, stringify(unsafeHtml));
}
/**
* A `style` sanitizer which converts untrusted `style` **string** into trusted string by removing
* dangerous content.
*
* This method parses the `style` and locates potentially dangerous content (such as urls and
* javascript) and removes it.
*
* It is possible to mark a string as trusted by calling {@link bypassSanitizationTrustStyle}.
*
* @param unsafeStyle untrusted `style`, typically from the user.
* @returns `style` string which is safe to bind to the `style` properties, because all of the
* dangerous javascript and urls have been removed.
*/
export function sanitizeStyle(unsafeStyle: any): string {
if (unsafeStyle instanceof String && (unsafeStyle as TrustedStyleString)[BRAND] === 'Style') {
return unsafeStyle.toString();
}
return _sanitizeStyle(stringify(unsafeStyle));
}
/**
* A `url` sanitizer which converts untrusted `url` **string** into trusted string by removing
* dangerous
* content.
*
* This method parses the `url` and locates potentially dangerous content (such as javascript) and
* removes it.
*
* It is possible to mark a string as trusted by calling {@link bypassSanitizationTrustUrl}.
*
* @param unsafeUrl untrusted `url`, typically from the user.
* @returns `url` string which is safe to bind to the `src` properties such as `<img src>`, because
* all of the dangerous javascript has been removed.
*/
export function sanitizeUrl(unsafeUrl: any): string {
if (unsafeUrl instanceof String && (unsafeUrl as TrustedUrlString)[BRAND] === 'Url') {
return unsafeUrl.toString();
}
return _sanitizeUrl(stringify(unsafeUrl));
}
/**
* A `url` sanitizer which only lets trusted `url`s through.
*
* This passes only `url`s marked trusted by calling {@link bypassSanitizationTrustResourceUrl}.
*
* @param unsafeResourceUrl untrusted `url`, typically from the user.
* @returns `url` string which is safe to bind to the `src` properties such as `<img src>`, because
* only trusted `url`s have been allowed to pass.
*/
export function sanitizeResourceUrl(unsafeResourceUrl: any): string {
if (unsafeResourceUrl instanceof String &&
(unsafeResourceUrl as TrustedResourceUrlString)[BRAND] === 'ResourceUrl') {
return unsafeResourceUrl.toString();
}
throw new Error('unsafe value used in a resource URL context (see http://g.co/ng/security#xss)');
}
/**
* A `script` sanitizer which only lets trusted javascript through.
*
* This passes only `script`s marked trusted by calling {@link bypassSanitizationTrustScript}.
*
* @param unsafeScript untrusted `script`, typically from the user.
* @returns `url` string which is safe to bind to the `<script>` element such as `<img src>`,
* because only trusted `scripts`s have been allowed to pass.
*/
export function sanitizeScript(unsafeScript: any): string {
if (unsafeScript instanceof String && (unsafeScript as TrustedScriptString)[BRAND] === 'Script') {
return unsafeScript.toString();
}
throw new Error('unsafe value used in a script context');
}
/**
* Mark `html` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link htmlSanitizer} to be trusted implicitly.
*
* @param trustedHtml `html` string which needs to be implicitly trusted.
* @returns a `html` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustHtml(trustedHtml: string): TrustedHtmlString {
return bypassSanitizationTrustString(trustedHtml, 'Html');
}
/**
* Mark `style` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link styleSanitizer} to be trusted implicitly.
*
* @param trustedStyle `style` string which needs to be implicitly trusted.
* @returns a `style` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustStyle(trustedStyle: string): TrustedStyleString {
return bypassSanitizationTrustString(trustedStyle, 'Style');
}
/**
* Mark `script` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link scriptSanitizer} to be trusted implicitly.
*
* @param trustedScript `script` string which needs to be implicitly trusted.
* @returns a `script` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustScript(trustedScript: string): TrustedScriptString {
return bypassSanitizationTrustString(trustedScript, 'Script');
}
/**
* Mark `url` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link urlSanitizer} to be trusted implicitly.
*
* @param trustedUrl `url` string which needs to be implicitly trusted.
* @returns a `url` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustUrl(trustedUrl: string): TrustedUrlString {
return bypassSanitizationTrustString(trustedUrl, 'Url');
}
/**
* Mark `url` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link resourceUrlSanitizer} to be trusted implicitly.
*
* @param trustedResourceUrl `url` string which needs to be implicitly trusted.
* @returns a `url` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustResourceUrl(trustedResourceUrl: string):
TrustedResourceUrlString {
return bypassSanitizationTrustString(trustedResourceUrl, 'ResourceUrl');
}
function bypassSanitizationTrustString(trustedString: string, mode: 'Html'): TrustedHtmlString;
function bypassSanitizationTrustString(trustedString: string, mode: 'Style'): TrustedStyleString;
function bypassSanitizationTrustString(trustedString: string, mode: 'Script'): TrustedScriptString;
function bypassSanitizationTrustString(trustedString: string, mode: 'Url'): TrustedUrlString;
function bypassSanitizationTrustString(
trustedString: string, mode: 'ResourceUrl'): TrustedResourceUrlString;
function bypassSanitizationTrustString(
trustedString: string,
mode: 'Html' | 'Style' | 'Script' | 'Url' | 'ResourceUrl'): TrustedString {
const trusted = new String(trustedString) as TrustedString;
trusted[BRAND] = mode;
return trusted;
}

View File

@ -6,9 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {isDevMode} from '@angular/core';
import {sanitizeUrl} from './url_sanitizer';
import {isDevMode} from '../application_ref';
import {_sanitizeUrl} from './url_sanitizer';
/**
@ -83,14 +82,14 @@ function hasBalancedQuotes(value: string) {
* Sanitizes the given untrusted CSS style property value (i.e. not an entire object, just a single
* value) and returns a value that is safe to use in a browser environment.
*/
export function sanitizeStyle(value: string): string {
export function _sanitizeStyle(value: string): string {
value = String(value).trim(); // Make sure it's actually a string.
if (!value) return '';
// Single url(...) values are supported, but only for URLs that sanitize cleanly. See above for
// reasoning behind this.
const urlMatch = value.match(URL_RE);
if ((urlMatch && sanitizeUrl(urlMatch[1]) === urlMatch[1]) ||
if ((urlMatch && _sanitizeUrl(urlMatch[1]) === urlMatch[1]) ||
value.match(SAFE_STYLE_VALUE) && hasBalancedQuotes(value)) {
return value; // Safe style values.
}

View File

@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {isDevMode} from '@angular/core';
import {isDevMode} from '../application_ref';
/**
* A pattern that recognizes a commonly useful subset of URLs that are safe.
@ -44,7 +43,7 @@ const SAFE_SRCSET_PATTERN = /^(?:(?:https?|file):|[^&:/?#]*(?:[/?#]|$))/gi;
const DATA_URL_PATTERN =
/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+\/]+=*$/i;
export function sanitizeUrl(url: string): string {
export function _sanitizeUrl(url: string): string {
url = String(url);
if (url.match(SAFE_URL_PATTERN) || url.match(DATA_URL_PATTERN)) return url;
@ -57,5 +56,5 @@ export function sanitizeUrl(url: string): string {
export function sanitizeSrcset(srcset: string): string {
srcset = String(srcset);
return srcset.split(',').map((srcset) => sanitizeUrl(srcset.trim())).join(', ');
return srcset.split(',').map((srcset) => _sanitizeUrl(srcset.trim())).join(', ');
}