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 {GetterFn as ɵGetterFn, MethodFn as ɵMethodFn, SetterFn as ɵSetterFn} from './reflection/types';
export {DirectRenderer as ɵDirectRenderer, RenderDebugInfo as ɵRenderDebugInfo} from './render/api';
export {sanitizeHtml as ɵsanitizeHtml} from './sanitization/html_sanitizer';
export {sanitizeStyle as ɵsanitizeStyle} from './sanitization/style_sanitizer';
export {sanitizeUrl as ɵsanitizeUrl} from './sanitization/url_sanitizer';
export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer';
export {_sanitizeStyle as ɵ_sanitizeStyle} from './sanitization/style_sanitizer';
export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer';
export {global as ɵglobal, looseIdentical as ɵlooseIdentical, stringify as ɵstringify} from './util';
export {makeDecorator as ɵmakeDecorator} from './util/decorators';
export {isObservable as ɵisObservable, isPromise as ɵisPromise} from './util/lang';

View File

@ -71,8 +71,15 @@ export {
ld as ɵld,
Pp as ɵPp,
} 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
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);
/**
* 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.
@ -689,20 +694,22 @@ export function elementEnd() {
* Updates the value of removes an attribute on an Element.
*
* @param number index The index of the element in the data array
* @param string name The name of the attribute.
* @param any value The attribute is removed when value is `null` or `undefined`.
* @param name name The name of the attribute.
* @param value value The attribute is removed when value is `null` or `undefined`.
* 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) {
const element: LElementNode = data[index];
if (value == null) {
isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) :
element.native.removeAttribute(name);
} else {
isProceduralRenderer(renderer) ?
renderer.setAttribute(element.native, name, stringify(value)) :
element.native.setAttribute(name, stringify(value));
const strValue = sanitizer == null ? stringify(value) : sanitizer(value);
isProceduralRenderer(renderer) ? renderer.setAttribute(element.native, name, strValue) :
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
* renaming as part of minification.
* @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;
const node = data[index] as LElementNode;
const tNode = node.tNode !;
@ -737,6 +746,7 @@ export function elementProperty<T>(index: number, propName: string, value: T | N
setInputsForProperty(dataValue, value);
markDirtyIfOnPush(node);
} else {
value = (sanitizer != null ? sanitizer(value) : stringify(value)) as any;
const native = node.native;
isProceduralRenderer(renderer) ? renderer.setProperty(native, 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
* renaming as part of minification.
* @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>(
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) {
const lElement = data[index] as LElementNode;
if (value == null) {
@ -853,7 +870,9 @@ export function elementStyle<T>(
renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) :
lElement.native.style.removeProperty(styleName);
} 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) ?
renderer.setStyle(lElement.native, styleName, strValue, RendererStyleFlags3.DashCase) :
lElement.native.style.setProperty(styleName, strValue);

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(', ');
}

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';
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 let containerEl: HTMLElement = null !;
let host: LElementNode|null;

View File

@ -8,7 +8,7 @@
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', () => {
@ -26,62 +26,62 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer';
afterEach(() => { console.warn = originalLog; });
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>');
expect(logMsgs).toEqual([]);
});
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>');
});
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', () => {
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>');
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:evil="something">t</a>')).toEqual('<a>t</a>');
expect(_sanitizeHtml(defaultDoc, '<a xlink:href="javascript:foo()">t</a>'))
.toEqual('<a xlink:href="unsafe:javascript:foo()">t</a>');
});
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>');
});
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">');
});
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', () => {
expect(sanitizeHtml(defaultDoc, '<!-- comments? -->no.')).toEqual('no.');
expect(sanitizeHtml(defaultDoc, '<?pi nodes?>no.')).toEqual('no.');
expect(_sanitizeHtml(defaultDoc, '<!-- comments? -->no.')).toEqual('no.');
expect(_sanitizeHtml(defaultDoc, '<?pi nodes?>no.')).toEqual('no.');
expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/);
});
it('supports sanitizing escaped entities', () => {
expect(sanitizeHtml(defaultDoc, '&#128640;')).toEqual('&#128640;');
expect(_sanitizeHtml(defaultDoc, '&#128640;')).toEqual('&#128640;');
expect(logMsgs).toEqual([]);
});
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>');
expect(logMsgs).toEqual([]);
});
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>');
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>Hello < World</p>')).toEqual('<p>Hello &lt; World</p>');
expect(_sanitizeHtml(defaultDoc, '<p alt="% &amp; &quot; !">Hello</p>'))
.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) {
it(`${tag}`,
() => { expect(sanitizeHtml(defaultDoc, `<${tag}>evil!</${tag}>`)).toEqual('evil!'); });
() => { expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!</${tag}>`)).toEqual('evil!'); });
}
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) {
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
// result in a timeout error for the test.
try {
sanitizeHtml(defaultDoc, '<form><input name="parentNode" /></form>');
_sanitizeHtml(defaultDoc, '<form><input name="parentNode" /></form>');
} catch (e) {
// depending on the browser, we might ge an exception
}
try {
sanitizeHtml(defaultDoc, '<form><input name="nextSibling" /></form>');
_sanitizeHtml(defaultDoc, '<form><input name="nextSibling" /></form>');
} catch (e) {
// depending on the browser, we might ge an exception
}
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) {
// depending on the browser, we might ge an exception
}
@ -136,7 +137,7 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer';
// See
// https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
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;
if (window) {
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
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)//">'))
.toEqual(
isDOMParserAvailable() ?
@ -161,7 +162,7 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer';
if (browserDetection.isWebkit) {
it('should prevent mXSS attacks', function() {
// 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>/);
});
}

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 {sanitizeStyle} from '../../src/sanitization/style_sanitizer';
import {_sanitizeStyle} from '../../src/sanitization/style_sanitizer';
{
t.describe('Style sanitizer', () => {
@ -23,7 +23,7 @@ import {sanitizeStyle} from '../../src/sanitization/style_sanitizer';
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', () => {
expectSanitize('').toEqual('');

View File

@ -8,7 +8,7 @@
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', () => {
@ -24,7 +24,7 @@ import {sanitizeSrcset, sanitizeUrl} from '../../src/sanitization/url_sanitizer'
afterEach(() => { console.warn = originalLog; });
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/);
});
@ -49,7 +49,7 @@ import {sanitizeSrcset, sanitizeUrl} from '../../src/sanitization/url_sanitizer'
'data:audio/opus;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/',
];
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/',
];
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
*/
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';
@ -156,11 +156,11 @@ export class DomSanitizerImpl extends DomSanitizer {
case SecurityContext.HTML:
if (value instanceof SafeHtmlImpl) return value.changingThisBreaksApplicationSecurity;
this.checkNotSafeValue(value, 'HTML');
return sanitizeHtml(this._doc, String(value));
return _sanitizeHtml(this._doc, String(value));
case SecurityContext.STYLE:
if (value instanceof SafeStyleImpl) return value.changingThisBreaksApplicationSecurity;
this.checkNotSafeValue(value, 'Style');
return sanitizeStyle(value as string);
return _sanitizeStyle(value as string);
case SecurityContext.SCRIPT:
if (value instanceof SafeScriptImpl) return value.changingThisBreaksApplicationSecurity;
this.checkNotSafeValue(value, 'Script');
@ -171,7 +171,7 @@ export class DomSanitizerImpl extends DomSanitizer {
return value.changingThisBreaksApplicationSecurity;
}
this.checkNotSafeValue(value, 'URL');
return sanitizeUrl(String(value));
return _sanitizeUrl(String(value));
case SecurityContext.RESOURCE_URL:
if (value instanceof SafeResourceUrlImpl) {
return value.changingThisBreaksApplicationSecurity;