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

@ -18,9 +18,9 @@ export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} fr
export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities'; export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities';
export {GetterFn as ɵGetterFn, MethodFn as ɵMethodFn, SetterFn as ɵSetterFn} from './reflection/types'; export {GetterFn as ɵGetterFn, MethodFn as ɵMethodFn, SetterFn as ɵSetterFn} from './reflection/types';
export {DirectRenderer as ɵDirectRenderer, RenderDebugInfo as ɵRenderDebugInfo} from './render/api'; export {DirectRenderer as ɵDirectRenderer, RenderDebugInfo as ɵRenderDebugInfo} from './render/api';
export {sanitizeHtml as ɵsanitizeHtml} from './sanitization/html_sanitizer'; export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer';
export {sanitizeStyle as ɵsanitizeStyle} from './sanitization/style_sanitizer'; export {_sanitizeStyle as ɵ_sanitizeStyle} from './sanitization/style_sanitizer';
export {sanitizeUrl as ɵsanitizeUrl} from './sanitization/url_sanitizer'; export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer';
export {global as ɵglobal, looseIdentical as ɵlooseIdentical, stringify as ɵstringify} from './util'; export {global as ɵglobal, looseIdentical as ɵlooseIdentical, stringify as ɵstringify} from './util';
export {makeDecorator as ɵmakeDecorator} from './util/decorators'; export {makeDecorator as ɵmakeDecorator} from './util/decorators';
export {isObservable as ɵisObservable, isPromise as ɵisPromise} from './util/lang'; export {isObservable as ɵisObservable, isPromise as ɵisPromise} from './util/lang';

View File

@ -71,8 +71,15 @@ export {
ld as ɵld, ld as ɵld,
Pp as ɵPp, Pp as ɵPp,
} from './render3/index'; } from './render3/index';
export {
bypassSanitizationTrustHtml as ɵbypassSanitizationTrustHtml,
bypassSanitizationTrustStyle as ɵbypassSanitizationTrustStyle,
bypassSanitizationTrustScript as ɵbypassSanitizationTrustScript,
bypassSanitizationTrustUrl as ɵbypassSanitizationTrustUrl,
bypassSanitizationTrustResourceUrl as ɵbypassSanitizationTrustResourceUrl,
sanitizeHtml as ɵsanitizeHtml,
sanitizeStyle as ɵsanitizeStyle,
sanitizeUrl as ɵsanitizeUrl,
sanitizeResourceUrl as ɵsanitizeResourceUrl,
} from './sanitization/sanitization';
// clang-format on // clang-format on
export {htmlSanitizer as ɵhtmlSanitizer} from './sanitization/html_sanitizer';
export {styleSanitizer as ɵstyleSanitizer} from './sanitization/style_sanitizer';
export {urlSanitizer as ɵurlSanitizer, resourceUrlSanitizer as ɵresourceUrlSanitizer} from './sanitization/url_sanitizer';

View File

@ -38,6 +38,11 @@ export const NG_HOST_SYMBOL = '__ngHostLNode__';
*/ */
const _CLEAN_PROMISE = Promise.resolve(null); const _CLEAN_PROMISE = Promise.resolve(null);
/**
* Function used to sanitize the value before writing it into the renderer.
*/
export type Sanitizer = (value: any) => string;
/** /**
* This property gets set before entering a template. * This property gets set before entering a template.
@ -689,20 +694,22 @@ export function elementEnd() {
* Updates the value of removes an attribute on an Element. * Updates the value of removes an attribute on an Element.
* *
* @param number index The index of the element in the data array * @param number index The index of the element in the data array
* @param string name The name of the attribute. * @param name name The name of the attribute.
* @param any value The attribute is removed when value is `null` or `undefined`. * @param value value The attribute is removed when value is `null` or `undefined`.
* Otherwise the attribute value is set to the stringified value. * Otherwise the attribute value is set to the stringified value.
* @param sanitizer An optional function used to sanitize the value.
*/ */
export function elementAttribute(index: number, name: string, value: any): void { export function elementAttribute(
index: number, name: string, value: any, sanitizer?: Sanitizer): void {
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
const element: LElementNode = data[index]; const element: LElementNode = data[index];
if (value == null) { if (value == null) {
isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) : isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) :
element.native.removeAttribute(name); element.native.removeAttribute(name);
} else { } else {
isProceduralRenderer(renderer) ? const strValue = sanitizer == null ? stringify(value) : sanitizer(value);
renderer.setAttribute(element.native, name, stringify(value)) : isProceduralRenderer(renderer) ? renderer.setAttribute(element.native, name, strValue) :
element.native.setAttribute(name, stringify(value)); element.native.setAttribute(name, strValue);
} }
} }
} }
@ -718,9 +725,11 @@ export function elementAttribute(index: number, name: string, value: any): void
* @param propName Name of property. Because it is going to DOM, this is not subject to * @param propName Name of property. Because it is going to DOM, this is not subject to
* renaming as part of minification. * renaming as part of minification.
* @param value New value to write. * @param value New value to write.
* @param sanitizer An optional function used to sanitize the value.
*/ */
export function elementProperty<T>(index: number, propName: string, value: T | NO_CHANGE): void { export function elementProperty<T>(
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: Sanitizer): void {
if (value === NO_CHANGE) return; if (value === NO_CHANGE) return;
const node = data[index] as LElementNode; const node = data[index] as LElementNode;
const tNode = node.tNode !; const tNode = node.tNode !;
@ -737,6 +746,7 @@ export function elementProperty<T>(index: number, propName: string, value: T | N
setInputsForProperty(dataValue, value); setInputsForProperty(dataValue, value);
markDirtyIfOnPush(node); markDirtyIfOnPush(node);
} else { } else {
value = (sanitizer != null ? sanitizer(value) : stringify(value)) as any;
const native = node.native; const native = node.native;
isProceduralRenderer(renderer) ? renderer.setProperty(native, propName, value) : isProceduralRenderer(renderer) ? renderer.setProperty(native, propName, value) :
(native.setProperty ? native.setProperty(propName, value) : (native.setProperty ? native.setProperty(propName, value) :
@ -842,10 +852,17 @@ export function elementClass<T>(index: number, className: string, value: T | NO_
* @param styleName Name of property. Because it is going to DOM this is not subject to * @param styleName Name of property. Because it is going to DOM this is not subject to
* renaming as part of minification. * renaming as part of minification.
* @param value New value to write (null to remove). * @param value New value to write (null to remove).
* @param suffix Suffix to add to style's value (optional). * @param suffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param sanitizer An optional function used to transform the value typically used for
* sanitization.
*/ */
export function elementStyle<T>( export function elementStyle<T>(
index: number, styleName: string, value: T | NO_CHANGE, suffix?: string): void { index: number, styleName: string, value: T | NO_CHANGE, suffix?: string): void;
export function elementStyle<T>(
index: number, styleName: string, value: T | NO_CHANGE, sanitizer?: Sanitizer): void;
export function elementStyle<T>(
index: number, styleName: string, value: T | NO_CHANGE,
suffixOrSanitizer?: string | Sanitizer): void {
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
const lElement = data[index] as LElementNode; const lElement = data[index] as LElementNode;
if (value == null) { if (value == null) {
@ -853,7 +870,9 @@ export function elementStyle<T>(
renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) : renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) :
lElement.native.style.removeProperty(styleName); lElement.native.style.removeProperty(styleName);
} else { } else {
const strValue = suffix ? stringify(value) + suffix : stringify(value); let strValue =
typeof suffixOrSanitizer == 'function' ? suffixOrSanitizer(value) : stringify(value);
if (typeof suffixOrSanitizer == 'string') strValue = strValue + suffixOrSanitizer;
isProceduralRenderer(renderer) ? isProceduralRenderer(renderer) ?
renderer.setStyle(lElement.native, styleName, strValue, RendererStyleFlags3.DashCase) : renderer.setStyle(lElement.native, styleName, strValue, RendererStyleFlags3.DashCase) :
lElement.native.style.setProperty(styleName, strValue); lElement.native.style.setProperty(styleName, strValue);

View File

@ -6,10 +6,9 @@
* found in the LICENSE file at https://angular.io/license * 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 {InertBodyHelper} from './inert_body';
import {sanitizeSrcset, sanitizeUrl} from './url_sanitizer'; import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
function tagSet(tags: string): {[k: string]: boolean} { function tagSet(tags: string): {[k: string]: boolean} {
const res: {[k: string]: boolean} = {}; const res: {[k: string]: boolean} = {};
@ -143,21 +142,17 @@ class SanitizingHtmlSerializer {
for (let i = 0; i < elAttrs.length; i++) { for (let i = 0; i < elAttrs.length; i++) {
const elAttr = elAttrs.item(i); const elAttr = elAttrs.item(i);
const attrName = elAttr.name; const attrName = elAttr.name;
let value = elAttr.value;
const lower = attrName.toLowerCase(); const lower = attrName.toLowerCase();
if (!VALID_ATTRS.hasOwnProperty(lower)) { if (!VALID_ATTRS.hasOwnProperty(lower)) {
this.sanitizedSomething = true; this.sanitizedSomething = true;
continue; continue;
} }
let value = elAttr.value;
// TODO(martinprobst): Special case image URIs for data:image/... // 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); if (SRCSET_ATTRS[lower]) value = sanitizeSrcset(value);
this.buf.push(' '); this.buf.push(' ', attrName, '="', encodeEntities(value), '"');
this.buf.push(attrName); }
this.buf.push('="');
this.buf.push(encodeEntities(value));
this.buf.push('"');
};
this.buf.push('>'); this.buf.push('>');
} }
@ -173,7 +168,9 @@ class SanitizingHtmlSerializer {
private chars(chars: string) { this.buf.push(encodeEntities(chars)); } private chars(chars: string) { this.buf.push(encodeEntities(chars)); }
checkClobberedElement(node: Node, nextNode: Node): Node { 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( throw new Error(
`Failed to sanitize html because the element is clobbered: ${(node as Element).outerHTML}`); `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 * Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to
* the DOM in a browser environment. * 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; let inertBodyElement: HTMLElement|null = null;
try { try {
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc); inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc);
@ -259,8 +256,8 @@ export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
} }
function getTemplateContent(el: Node): Node|null { 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'; 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {isDevMode} from '@angular/core'; import {isDevMode} from '../application_ref';
import {_sanitizeUrl} from './url_sanitizer';
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 * 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. * 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. value = String(value).trim(); // Make sure it's actually a string.
if (!value) return ''; if (!value) return '';
// Single url(...) values are supported, but only for URLs that sanitize cleanly. See above for // Single url(...) values are supported, but only for URLs that sanitize cleanly. See above for
// reasoning behind this. // reasoning behind this.
const urlMatch = value.match(URL_RE); 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)) { value.match(SAFE_STYLE_VALUE) && hasBalancedQuotes(value)) {
return value; // Safe style values. return value; // Safe style values.
} }

View File

@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license * 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. * 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 = 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; /^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); url = String(url);
if (url.match(SAFE_URL_PATTERN) || url.match(DATA_URL_PATTERN)) return 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 { export function sanitizeSrcset(srcset: string): string {
srcset = String(srcset); srcset = String(srcset);
return srcset.split(',').map((srcset) => sanitizeUrl(srcset.trim())).join(', '); return srcset.split(',').map((srcset) => _sanitizeUrl(srcset.trim())).join(', ');
} }

View File

@ -0,0 +1,72 @@
/**
* @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 {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core';
import * as $r3$ from '../../../src/core_render3_private_export';
import {getHostElement} from '../../../src/render3/index';
import {renderComponent, toHtml} from '../render_util';
/**
* NORMATIVE => /NORMATIVE: Designates what the compiler is expected to generate.
*
* All local variable names are considered non-normative (informative). They should be
* wrapped in $ on each end to simplify testing on the compiler side.
*/
describe('compiler sanitization', () => {
type $boolean$ = boolean;
it('should translate DOM structure', () => {
type $MyComponent$ = MyComponent;
@Component({
selector: 'my-component',
template: `<div [innerHTML]="innerHTML"></div>` +
`<img [style.background-image]="style" [src]="src">` +
`<script [attr.src]=src></script>`
})
class MyComponent {
innerHTML: string = '<frame></frame>';
style: string = `url("http://evil")`;
url: string = 'javascript:evil()';
// NORMATIVE
static ngComponentDef = $r3$.ɵdefineComponent({
type: MyComponent,
tag: 'my-component',
factory: function MyComponent_Factory() { return new MyComponent(); },
template: function MyComponent_Template(ctx: $MyComponent$, cm: $boolean$) {
if (cm) {
$r3$.ɵE(0, 'div');
$r3$.ɵe();
$r3$.ɵE(1, 'img');
$r3$.ɵe();
}
$r3$.ɵp(0, 'innerHTML', $r3$.ɵb(ctx.innerHTML), $r3$.ɵsanitizeHtml);
$r3$.ɵs(1, 'background-image', $r3$.ɵb(ctx.style), $r3$.ɵsanitizeStyle);
$r3$.ɵp(1, 'src', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);
$r3$.ɵa(1, 'srcset', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);
}
});
// /NORMATIVE
}
const myComponent = renderComponent(MyComponent);
const div = getHostElement(myComponent).querySelector('div') !;
// because sanitizer removed it is working.
expect(div.innerHTML).toEqual('');
const img = getHostElement(myComponent).querySelector('img') !;
// because sanitizer removed it is working.
expect(img.getAttribute('src')).toEqual('unsafe:javascript:evil()');
// because sanitizer removed it is working.
expect(img.style.getPropertyValue('background-image')).toEqual('');
// because sanitizer removed it is working.
expect(img.getAttribute('srcset')).toEqual('unsafe:javascript:evil()');
});
});

View File

@ -0,0 +1,65 @@
/**
* @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 {elementAttribute, elementEnd, elementProperty, elementStart, elementStyle, renderTemplate} from '../../src/render3/instructions';
import {LElementNode, LNode} from '../../src/render3/interfaces/node';
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
import {TemplateFixture} from './render_util';
describe('instructions', () => {
function createDiv() {
elementStart(0, 'div');
elementEnd();
}
describe('elementAttribute', () => {
it('should use sanitizer function', () => {
const t = new TemplateFixture(createDiv);
t.update(() => elementAttribute(0, 'title', 'javascript:true', sanitizeUrl));
expect(t.html).toEqual('<div title="unsafe:javascript:true"></div>');
t.update(
() => elementAttribute(
0, 'title', bypassSanitizationTrustUrl('javascript:true'), sanitizeUrl));
expect(t.html).toEqual('<div title="javascript:true"></div>');
});
});
describe('elementProperty', () => {
it('should use sanitizer function', () => {
const t = new TemplateFixture(createDiv);
t.update(() => elementProperty(0, 'title', 'javascript:true', sanitizeUrl));
expect(t.html).toEqual('<div title="unsafe:javascript:true"></div>');
t.update(
() => elementProperty(
0, 'title', bypassSanitizationTrustUrl('javascript:false'), sanitizeUrl));
expect(t.html).toEqual('<div title="javascript:false"></div>');
});
});
describe('elementStyle', () => {
it('should use sanitizer function', () => {
const t = new TemplateFixture(createDiv);
t.update(() => elementStyle(0, 'background-image', 'url("http://server")', sanitizeStyle));
// nothing is set because sanitizer suppresses it.
expect(t.html).toEqual('<div></div>');
t.update(
() => elementStyle(
0, 'background-image', bypassSanitizationTrustStyle('url("http://server")'),
sanitizeStyle));
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('url("http://server")');
});
});
});

View File

@ -16,6 +16,58 @@ import {RElement, RText, Renderer3, RendererFactory3, domRendererFactory3} from
import {getRendererFactory2} from './imported_renderer2'; import {getRendererFactory2} from './imported_renderer2';
function noop() {}
/**
* Fixture for testing template functions in a convenient way.
*
* This fixture allows:
* - specifying the creation block and update block as two separate functions,
* - maintaining the template state between invocations,
* - access to the render `html`.
*/
export class TemplateFixture {
hostElement: HTMLElement;
hostNode: LElementNode;
/**
*
* @param createBlock Instructions which go into the creation block:
* `if (creationMode) { __here__ }`.
* @param updateBlock Optional instructions which go after the creation block:
* `if (creationMode) { ... } __here__`.
*/
constructor(private createBlock: () => void, private updateBlock: () => void = noop) {
this.updateBlock = updateBlock || function() {};
this.hostElement = document.createElement('div');
this.hostNode = renderTemplate(this.hostElement, (ctx: any, cm: boolean) => {
if (cm) {
this.createBlock();
}
this.updateBlock();
}, null !, domRendererFactory3, null);
}
/**
* Update the existing template
*
* @param updateBlock Optional update block.
*/
update(updateBlock?: () => void): void {
renderTemplate(
this.hostNode.native, updateBlock || this.updateBlock, null !, domRendererFactory3,
this.hostNode);
}
/**
* Current state of rendered HTML.
*/
get html(): string {
return (this.hostNode.native as any as Element).innerHTML.replace(/ style=""/g, '');
}
}
export const document = ((global || window) as any).document; export const document = ((global || window) as any).document;
export let containerEl: HTMLElement = null !; export let containerEl: HTMLElement = null !;
let host: LElementNode|null; let host: LElementNode|null;

View File

@ -8,7 +8,7 @@
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {sanitizeHtml} from '../../src/sanitization/html_sanitizer'; import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
{ {
describe('HTML sanitizer', () => { describe('HTML sanitizer', () => {
@ -26,62 +26,62 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer';
afterEach(() => { console.warn = originalLog; }); afterEach(() => { console.warn = originalLog; });
it('serializes nested structures', () => { it('serializes nested structures', () => {
expect(sanitizeHtml(defaultDoc, '<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>')) expect(_sanitizeHtml(defaultDoc, '<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>'))
.toEqual('<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>'); .toEqual('<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>');
expect(logMsgs).toEqual([]); expect(logMsgs).toEqual([]);
}); });
it('serializes self closing elements', () => { it('serializes self closing elements', () => {
expect(sanitizeHtml(defaultDoc, '<p>Hello <br> World</p>')) expect(_sanitizeHtml(defaultDoc, '<p>Hello <br> World</p>'))
.toEqual('<p>Hello <br> World</p>'); .toEqual('<p>Hello <br> World</p>');
}); });
it('supports namespaced elements', it('supports namespaced elements',
() => { expect(sanitizeHtml(defaultDoc, 'a<my:hr/><my:div>b</my:div>c')).toEqual('abc'); }); () => { expect(_sanitizeHtml(defaultDoc, 'a<my:hr/><my:div>b</my:div>c')).toEqual('abc'); });
it('supports namespaced attributes', () => { it('supports namespaced attributes', () => {
expect(sanitizeHtml(defaultDoc, '<a xlink:href="something">t</a>')) expect(_sanitizeHtml(defaultDoc, '<a xlink:href="something">t</a>'))
.toEqual('<a xlink:href="something">t</a>'); .toEqual('<a xlink:href="something">t</a>');
expect(sanitizeHtml(defaultDoc, '<a xlink:evil="something">t</a>')).toEqual('<a>t</a>'); expect(_sanitizeHtml(defaultDoc, '<a xlink:evil="something">t</a>')).toEqual('<a>t</a>');
expect(sanitizeHtml(defaultDoc, '<a xlink:href="javascript:foo()">t</a>')) expect(_sanitizeHtml(defaultDoc, '<a xlink:href="javascript:foo()">t</a>'))
.toEqual('<a xlink:href="unsafe:javascript:foo()">t</a>'); .toEqual('<a xlink:href="unsafe:javascript:foo()">t</a>');
}); });
it('supports HTML5 elements', () => { it('supports HTML5 elements', () => {
expect(sanitizeHtml(defaultDoc, '<main><summary>Works</summary></main>')) expect(_sanitizeHtml(defaultDoc, '<main><summary>Works</summary></main>'))
.toEqual('<main><summary>Works</summary></main>'); .toEqual('<main><summary>Works</summary></main>');
}); });
it('sanitizes srcset attributes', () => { it('sanitizes srcset attributes', () => {
expect(sanitizeHtml(defaultDoc, '<img srcset="/foo.png 400px, javascript:evil() 23px">')) expect(_sanitizeHtml(defaultDoc, '<img srcset="/foo.png 400px, javascript:evil() 23px">'))
.toEqual('<img srcset="/foo.png 400px, unsafe:javascript:evil() 23px">'); .toEqual('<img srcset="/foo.png 400px, unsafe:javascript:evil() 23px">');
}); });
it('supports sanitizing plain text', it('supports sanitizing plain text',
() => { expect(sanitizeHtml(defaultDoc, 'Hello, World')).toEqual('Hello, World'); }); () => { expect(_sanitizeHtml(defaultDoc, 'Hello, World')).toEqual('Hello, World'); });
it('ignores non-element, non-attribute nodes', () => { it('ignores non-element, non-attribute nodes', () => {
expect(sanitizeHtml(defaultDoc, '<!-- comments? -->no.')).toEqual('no.'); expect(_sanitizeHtml(defaultDoc, '<!-- comments? -->no.')).toEqual('no.');
expect(sanitizeHtml(defaultDoc, '<?pi nodes?>no.')).toEqual('no.'); expect(_sanitizeHtml(defaultDoc, '<?pi nodes?>no.')).toEqual('no.');
expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/); expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/);
}); });
it('supports sanitizing escaped entities', () => { it('supports sanitizing escaped entities', () => {
expect(sanitizeHtml(defaultDoc, '&#128640;')).toEqual('&#128640;'); expect(_sanitizeHtml(defaultDoc, '&#128640;')).toEqual('&#128640;');
expect(logMsgs).toEqual([]); expect(logMsgs).toEqual([]);
}); });
it('does not warn when just re-encoding text', () => { it('does not warn when just re-encoding text', () => {
expect(sanitizeHtml(defaultDoc, '<p>Hellö Wörld</p>')) expect(_sanitizeHtml(defaultDoc, '<p>Hellö Wörld</p>'))
.toEqual('<p>Hell&#246; W&#246;rld</p>'); .toEqual('<p>Hell&#246; W&#246;rld</p>');
expect(logMsgs).toEqual([]); expect(logMsgs).toEqual([]);
}); });
it('escapes entities', () => { it('escapes entities', () => {
expect(sanitizeHtml(defaultDoc, '<p>Hello &lt; World</p>')) expect(_sanitizeHtml(defaultDoc, '<p>Hello &lt; World</p>'))
.toEqual('<p>Hello &lt; World</p>'); .toEqual('<p>Hello &lt; World</p>');
expect(sanitizeHtml(defaultDoc, '<p>Hello < World</p>')).toEqual('<p>Hello &lt; World</p>'); expect(_sanitizeHtml(defaultDoc, '<p>Hello < World</p>')).toEqual('<p>Hello &lt; World</p>');
expect(sanitizeHtml(defaultDoc, '<p alt="% &amp; &quot; !">Hello</p>')) expect(_sanitizeHtml(defaultDoc, '<p alt="% &amp; &quot; !">Hello</p>'))
.toEqual('<p alt="% &amp; &#34; !">Hello</p>'); // NB: quote encoded as ASCII &#34;. .toEqual('<p alt="% &amp; &#34; !">Hello</p>'); // NB: quote encoded as ASCII &#34;.
}); });
@ -93,11 +93,11 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer';
for (const tag of dangerousTags) { for (const tag of dangerousTags) {
it(`${tag}`, it(`${tag}`,
() => { expect(sanitizeHtml(defaultDoc, `<${tag}>evil!</${tag}>`)).toEqual('evil!'); }); () => { expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!</${tag}>`)).toEqual('evil!'); });
} }
it(`swallows frame entirely`, () => { it(`swallows frame entirely`, () => {
expect(sanitizeHtml(defaultDoc, `<frame>evil!</frame>`)).not.toContain('<frame>'); expect(_sanitizeHtml(defaultDoc, `<frame>evil!</frame>`)).not.toContain('<frame>');
}); });
}); });
@ -106,7 +106,7 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer';
for (const attr of dangerousAttrs) { for (const attr of dangerousAttrs) {
it(`${attr}`, () => { it(`${attr}`, () => {
expect(sanitizeHtml(defaultDoc, `<a ${attr}="x">evil!</a>`)).toEqual('<a>evil!</a>'); expect(_sanitizeHtml(defaultDoc, `<a ${attr}="x">evil!</a>`)).toEqual('<a>evil!</a>');
}); });
} }
}); });
@ -117,17 +117,18 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer';
// Anyway what we want to test is that browsers do not enter an infinite loop which would // Anyway what we want to test is that browsers do not enter an infinite loop which would
// result in a timeout error for the test. // result in a timeout error for the test.
try { try {
sanitizeHtml(defaultDoc, '<form><input name="parentNode" /></form>'); _sanitizeHtml(defaultDoc, '<form><input name="parentNode" /></form>');
} catch (e) { } catch (e) {
// depending on the browser, we might ge an exception // depending on the browser, we might ge an exception
} }
try { try {
sanitizeHtml(defaultDoc, '<form><input name="nextSibling" /></form>'); _sanitizeHtml(defaultDoc, '<form><input name="nextSibling" /></form>');
} catch (e) { } catch (e) {
// depending on the browser, we might ge an exception // depending on the browser, we might ge an exception
} }
try { try {
sanitizeHtml(defaultDoc, '<form><div><div><input name="nextSibling" /></div></div></form>'); _sanitizeHtml(
defaultDoc, '<form><div><div><input name="nextSibling" /></div></div></form>');
} catch (e) { } catch (e) {
// depending on the browser, we might ge an exception // depending on the browser, we might ge an exception
} }
@ -136,7 +137,7 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer';
// See // See
// https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449 // https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
it('should not allow JavaScript execution when creating inert document', () => { it('should not allow JavaScript execution when creating inert document', () => {
const output = sanitizeHtml(defaultDoc, '<svg><g onload="window.xxx = 100"></g></svg>'); const output = _sanitizeHtml(defaultDoc, '<svg><g onload="window.xxx = 100"></g></svg>');
const window = defaultDoc.defaultView; const window = defaultDoc.defaultView;
if (window) { if (window) {
expect(window.xxx).toBe(undefined); expect(window.xxx).toBe(undefined);
@ -148,7 +149,7 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer';
// See https://github.com/cure53/DOMPurify/releases/tag/0.6.7 // See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
it('should not allow JavaScript hidden in badly formed HTML to get through sanitization (Firefox bug)', it('should not allow JavaScript hidden in badly formed HTML to get through sanitization (Firefox bug)',
() => { () => {
expect(sanitizeHtml( expect(_sanitizeHtml(
defaultDoc, '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">')) defaultDoc, '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">'))
.toEqual( .toEqual(
isDOMParserAvailable() ? isDOMParserAvailable() ?
@ -161,7 +162,7 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer';
if (browserDetection.isWebkit) { if (browserDetection.isWebkit) {
it('should prevent mXSS attacks', function() { it('should prevent mXSS attacks', function() {
// In Chrome Canary 62, the ideographic space character is kept as a stringified HTML entity // In Chrome Canary 62, the ideographic space character is kept as a stringified HTML entity
expect(sanitizeHtml(defaultDoc, '<a href="&#x3000;javascript:alert(1)">CLICKME</a>')) expect(_sanitizeHtml(defaultDoc, '<a href="&#x3000;javascript:alert(1)">CLICKME</a>'))
.toMatch(/<a href="unsafe:(&#12288;)?javascript:alert\(1\)">CLICKME<\/a>/); .toMatch(/<a href="unsafe:(&#12288;)?javascript:alert\(1\)">CLICKME<\/a>/);
}); });
} }

View File

@ -0,0 +1,67 @@
/**
* @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 {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
describe('sanitization', () => {
class Wrap {
constructor(private value: string) {}
toString() { return this.value; }
}
it('should sanitize html', () => {
expect(sanitizeHtml('<div></div>')).toEqual('<div></div>');
expect(sanitizeHtml(new Wrap('<div></div>'))).toEqual('<div></div>');
expect(sanitizeHtml('<img src="javascript:true">'))
.toEqual('<img src="unsafe:javascript:true">');
expect(sanitizeHtml(new Wrap('<img src="javascript:true">')))
.toEqual('<img src="unsafe:javascript:true">');
expect(sanitizeHtml(bypassSanitizationTrustUrl('<img src="javascript:true">')))
.toEqual('<img src="unsafe:javascript:true">');
expect(sanitizeHtml(bypassSanitizationTrustHtml('<img src="javascript:true">')))
.toEqual('<img src="javascript:true">');
});
it('should sanitize url', () => {
expect(sanitizeUrl('http://server')).toEqual('http://server');
expect(sanitizeUrl(new Wrap('http://server'))).toEqual('http://server');
expect(sanitizeUrl('javascript:true')).toEqual('unsafe:javascript:true');
expect(sanitizeUrl(new Wrap('javascript:true'))).toEqual('unsafe:javascript:true');
expect(sanitizeUrl(bypassSanitizationTrustHtml('javascript:true')))
.toEqual('unsafe:javascript:true');
expect(sanitizeUrl(bypassSanitizationTrustUrl('javascript:true'))).toEqual('javascript:true');
});
it('should sanitize resourceUrl', () => {
const ERROR = 'unsafe value used in a resource URL context (see http://g.co/ng/security#xss)';
expect(() => sanitizeResourceUrl('http://server')).toThrowError(ERROR);
expect(() => sanitizeResourceUrl('javascript:true')).toThrowError(ERROR);
expect(() => sanitizeResourceUrl(bypassSanitizationTrustHtml('javascript:true')))
.toThrowError(ERROR);
expect(sanitizeResourceUrl(bypassSanitizationTrustResourceUrl('javascript:true')))
.toEqual('javascript:true');
});
it('should sanitize style', () => {
expect(sanitizeStyle('red')).toEqual('red');
expect(sanitizeStyle(new Wrap('red'))).toEqual('red');
expect(sanitizeStyle('url("http://server")')).toEqual('unsafe');
expect(sanitizeStyle(new Wrap('url("http://server")'))).toEqual('unsafe');
expect(sanitizeStyle(bypassSanitizationTrustHtml('url("http://server")'))).toEqual('unsafe');
expect(sanitizeStyle(bypassSanitizationTrustStyle('url("http://server")')))
.toEqual('url("http://server")');
});
it('should sanitize script', () => {
const ERROR = 'unsafe value used in a script context';
expect(() => sanitizeScript('true')).toThrowError(ERROR);
expect(() => sanitizeScript('true')).toThrowError(ERROR);
expect(() => sanitizeScript(bypassSanitizationTrustHtml('true'))).toThrowError(ERROR);
expect(sanitizeScript(bypassSanitizationTrustScript('true'))).toEqual('true');
});
});

View File

@ -8,7 +8,7 @@
import * as t from '@angular/core/testing/src/testing_internal'; import * as t from '@angular/core/testing/src/testing_internal';
import {sanitizeStyle} from '../../src/sanitization/style_sanitizer'; import {_sanitizeStyle} from '../../src/sanitization/style_sanitizer';
{ {
t.describe('Style sanitizer', () => { t.describe('Style sanitizer', () => {
@ -23,7 +23,7 @@ import {sanitizeStyle} from '../../src/sanitization/style_sanitizer';
afterEach(() => { console.warn = originalLog; }); afterEach(() => { console.warn = originalLog; });
function expectSanitize(v: string) { return t.expect(sanitizeStyle(v)); } function expectSanitize(v: string) { return t.expect(_sanitizeStyle(v)); }
t.it('sanitizes values', () => { t.it('sanitizes values', () => {
expectSanitize('').toEqual(''); expectSanitize('').toEqual('');

View File

@ -8,7 +8,7 @@
import * as t from '@angular/core/testing/src/testing_internal'; import * as t from '@angular/core/testing/src/testing_internal';
import {sanitizeSrcset, sanitizeUrl} from '../../src/sanitization/url_sanitizer'; import {_sanitizeUrl, sanitizeSrcset} from '../../src/sanitization/url_sanitizer';
{ {
t.describe('URL sanitizer', () => { t.describe('URL sanitizer', () => {
@ -24,7 +24,7 @@ import {sanitizeSrcset, sanitizeUrl} from '../../src/sanitization/url_sanitizer'
afterEach(() => { console.warn = originalLog; }); afterEach(() => { console.warn = originalLog; });
t.it('reports unsafe URLs', () => { t.it('reports unsafe URLs', () => {
t.expect(sanitizeUrl('javascript:evil()')).toBe('unsafe:javascript:evil()'); t.expect(_sanitizeUrl('javascript:evil()')).toBe('unsafe:javascript:evil()');
t.expect(logMsgs.join('\n')).toMatch(/sanitizing unsafe URL value/); t.expect(logMsgs.join('\n')).toMatch(/sanitizing unsafe URL value/);
}); });
@ -49,7 +49,7 @@ import {sanitizeSrcset, sanitizeUrl} from '../../src/sanitization/url_sanitizer'
'data:audio/opus;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', 'data:audio/opus;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
]; ];
for (const url of validUrls) { for (const url of validUrls) {
t.it(`valid ${url}`, () => t.expect(sanitizeUrl(url)).toEqual(url)); t.it(`valid ${url}`, () => t.expect(_sanitizeUrl(url)).toEqual(url));
} }
}); });
@ -73,7 +73,7 @@ import {sanitizeSrcset, sanitizeUrl} from '../../src/sanitization/url_sanitizer'
'data:application/x-msdownload;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', 'data:application/x-msdownload;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
]; ];
for (const url of invalidUrls) { for (const url of invalidUrls) {
t.it(`valid ${url}`, () => t.expect(sanitizeUrl(url)).toMatch(/^unsafe:/)); t.it(`valid ${url}`, () => t.expect(_sanitizeUrl(url)).toMatch(/^unsafe:/));
} }
}); });

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Inject, Injectable, Sanitizer, SecurityContext, ɵsanitizeHtml as sanitizeHtml, ɵsanitizeStyle as sanitizeStyle, ɵsanitizeUrl as sanitizeUrl} from '@angular/core'; import {Inject, Injectable, Sanitizer, SecurityContext, ɵ_sanitizeHtml as _sanitizeHtml, ɵ_sanitizeStyle as _sanitizeStyle, ɵ_sanitizeUrl as _sanitizeUrl} from '@angular/core';
import {DOCUMENT} from '../dom/dom_tokens'; import {DOCUMENT} from '../dom/dom_tokens';
@ -156,11 +156,11 @@ export class DomSanitizerImpl extends DomSanitizer {
case SecurityContext.HTML: case SecurityContext.HTML:
if (value instanceof SafeHtmlImpl) return value.changingThisBreaksApplicationSecurity; if (value instanceof SafeHtmlImpl) return value.changingThisBreaksApplicationSecurity;
this.checkNotSafeValue(value, 'HTML'); this.checkNotSafeValue(value, 'HTML');
return sanitizeHtml(this._doc, String(value)); return _sanitizeHtml(this._doc, String(value));
case SecurityContext.STYLE: case SecurityContext.STYLE:
if (value instanceof SafeStyleImpl) return value.changingThisBreaksApplicationSecurity; if (value instanceof SafeStyleImpl) return value.changingThisBreaksApplicationSecurity;
this.checkNotSafeValue(value, 'Style'); this.checkNotSafeValue(value, 'Style');
return sanitizeStyle(value as string); return _sanitizeStyle(value as string);
case SecurityContext.SCRIPT: case SecurityContext.SCRIPT:
if (value instanceof SafeScriptImpl) return value.changingThisBreaksApplicationSecurity; if (value instanceof SafeScriptImpl) return value.changingThisBreaksApplicationSecurity;
this.checkNotSafeValue(value, 'Script'); this.checkNotSafeValue(value, 'Script');
@ -171,7 +171,7 @@ export class DomSanitizerImpl extends DomSanitizer {
return value.changingThisBreaksApplicationSecurity; return value.changingThisBreaksApplicationSecurity;
} }
this.checkNotSafeValue(value, 'URL'); this.checkNotSafeValue(value, 'URL');
return sanitizeUrl(String(value)); return _sanitizeUrl(String(value));
case SecurityContext.RESOURCE_URL: case SecurityContext.RESOURCE_URL:
if (value instanceof SafeResourceUrlImpl) { if (value instanceof SafeResourceUrlImpl) {
return value.changingThisBreaksApplicationSecurity; return value.changingThisBreaksApplicationSecurity;