From 6d1367d29707c2c449fda1adfa8a7177693f680b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mis=CC=8Cko=20Hevery?= Date: Thu, 1 Mar 2018 17:14:01 -0800 Subject: [PATCH] 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 --- packages/core/src/core_private_export.ts | 6 +- .../core/src/core_render3_private_export.ts | 15 +- packages/core/src/render3/instructions.ts | 39 ++- .../core/src/sanitization/html_sanitizer.ts | 27 +- .../core/src/sanitization/sanitization.ts | 237 ++++++++++++++++++ .../core/src/sanitization/style_sanitizer.ts | 9 +- .../core/src/sanitization/url_sanitizer.ts | 7 +- .../compiler_canonical/sanitize_spec.ts | 72 ++++++ .../core/test/render3/instructions_spec.ts | 65 +++++ packages/core/test/render3/render_util.ts | 52 ++++ .../test/sanitization/html_sanitizer_spec.ts | 53 ++-- .../test/sanitization/sanatization_spec.ts | 67 +++++ .../test/sanitization/style_sanitizer_spec.ts | 4 +- .../test/sanitization/url_sanitizer_spec.ts | 8 +- .../src/security/dom_sanitization_service.ts | 8 +- 15 files changed, 592 insertions(+), 77 deletions(-) create mode 100644 packages/core/src/sanitization/sanitization.ts create mode 100644 packages/core/test/render3/compiler_canonical/sanitize_spec.ts create mode 100644 packages/core/test/render3/instructions_spec.ts create mode 100644 packages/core/test/sanitization/sanatization_spec.ts diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 14166b5dfb..5ae5166e2a 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -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'; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index d13d4b212c..901a10f57b 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -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'; diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index ebe31d590e..786fb55b5e 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -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(index: number, propName: string, value: T | NO_CHANGE): void { +export function elementProperty( + 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(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(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( - 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( + index: number, styleName: string, value: T | NO_CHANGE, sanitizer?: Sanitizer): void; +export function elementStyle( + 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( 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); diff --git a/packages/core/src/sanitization/html_sanitizer.ts b/packages/core/src/sanitization/html_sanitizer.ts index 1f60b9e01f..13f5a353f3 100644 --- a/packages/core/src/sanitization/html_sanitizer.ts +++ b/packages/core/src/sanitization/html_sanitizer.ts @@ -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) ? (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'; } diff --git a/packages/core/src/sanitization/sanitization.ts b/packages/core/src/sanitization/sanitization.ts new file mode 100644 index 0000000000..2f647fe640 --- /dev/null +++ b/packages/core/src/sanitization/sanitization.ts @@ -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 ``, 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 ``, 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 `` + }) + class MyComponent { + innerHTML: string = ''; + 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()'); + }); + +}); \ No newline at end of file diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts new file mode 100644 index 0000000000..9240f5a14d --- /dev/null +++ b/packages/core/test/render3/instructions_spec.ts @@ -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('
'); + + t.update( + () => elementAttribute( + 0, 'title', bypassSanitizationTrustUrl('javascript:true'), sanitizeUrl)); + expect(t.html).toEqual('
'); + }); + }); + + describe('elementProperty', () => { + it('should use sanitizer function', () => { + const t = new TemplateFixture(createDiv); + + t.update(() => elementProperty(0, 'title', 'javascript:true', sanitizeUrl)); + expect(t.html).toEqual('
'); + + t.update( + () => elementProperty( + 0, 'title', bypassSanitizationTrustUrl('javascript:false'), sanitizeUrl)); + expect(t.html).toEqual('
'); + }); + }); + + 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('
'); + + 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")'); + }); + }); +}); diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 7ca0b73d70..2ae002f628 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -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; diff --git a/packages/core/test/sanitization/html_sanitizer_spec.ts b/packages/core/test/sanitization/html_sanitizer_spec.ts index e0a9e0ba2f..ab9cf05313 100644 --- a/packages/core/test/sanitization/html_sanitizer_spec.ts +++ b/packages/core/test/sanitization/html_sanitizer_spec.ts @@ -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, '

a

bcde
')) + expect(_sanitizeHtml(defaultDoc, '

a

bcde
')) .toEqual('

a

bcde
'); expect(logMsgs).toEqual([]); }); it('serializes self closing elements', () => { - expect(sanitizeHtml(defaultDoc, '

Hello
World

')) + expect(_sanitizeHtml(defaultDoc, '

Hello
World

')) .toEqual('

Hello
World

'); }); it('supports namespaced elements', - () => { expect(sanitizeHtml(defaultDoc, 'abc')).toEqual('abc'); }); + () => { expect(_sanitizeHtml(defaultDoc, 'abc')).toEqual('abc'); }); it('supports namespaced attributes', () => { - expect(sanitizeHtml(defaultDoc, 't')) + expect(_sanitizeHtml(defaultDoc, 't')) .toEqual('t'); - expect(sanitizeHtml(defaultDoc, 't')).toEqual('t'); - expect(sanitizeHtml(defaultDoc, 't')) + expect(_sanitizeHtml(defaultDoc, 't')).toEqual('t'); + expect(_sanitizeHtml(defaultDoc, 't')) .toEqual('t'); }); it('supports HTML5 elements', () => { - expect(sanitizeHtml(defaultDoc, '
Works
')) + expect(_sanitizeHtml(defaultDoc, '
Works
')) .toEqual('
Works
'); }); it('sanitizes srcset attributes', () => { - expect(sanitizeHtml(defaultDoc, '')) + expect(_sanitizeHtml(defaultDoc, '')) .toEqual(''); }); 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, 'no.')).toEqual('no.'); - expect(sanitizeHtml(defaultDoc, 'no.')).toEqual('no.'); + expect(_sanitizeHtml(defaultDoc, 'no.')).toEqual('no.'); + expect(_sanitizeHtml(defaultDoc, 'no.')).toEqual('no.'); expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/); }); it('supports sanitizing escaped entities', () => { - expect(sanitizeHtml(defaultDoc, '🚀')).toEqual('🚀'); + expect(_sanitizeHtml(defaultDoc, '🚀')).toEqual('🚀'); expect(logMsgs).toEqual([]); }); it('does not warn when just re-encoding text', () => { - expect(sanitizeHtml(defaultDoc, '

Hellö Wörld

')) + expect(_sanitizeHtml(defaultDoc, '

Hellö Wörld

')) .toEqual('

Hellö Wörld

'); expect(logMsgs).toEqual([]); }); it('escapes entities', () => { - expect(sanitizeHtml(defaultDoc, '

Hello < World

')) + expect(_sanitizeHtml(defaultDoc, '

Hello < World

')) .toEqual('

Hello < World

'); - expect(sanitizeHtml(defaultDoc, '

Hello < World

')).toEqual('

Hello < World

'); - expect(sanitizeHtml(defaultDoc, '

Hello

')) + expect(_sanitizeHtml(defaultDoc, '

Hello < World

')).toEqual('

Hello < World

'); + expect(_sanitizeHtml(defaultDoc, '

Hello

')) .toEqual('

Hello

'); // NB: quote encoded as ASCII ". }); @@ -93,11 +93,11 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer'; for (const tag of dangerousTags) { it(`${tag}`, - () => { expect(sanitizeHtml(defaultDoc, `<${tag}>evil!`)).toEqual('evil!'); }); + () => { expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!`)).toEqual('evil!'); }); } it(`swallows frame entirely`, () => { - expect(sanitizeHtml(defaultDoc, `evil!`)).not.toContain(''); + expect(_sanitizeHtml(defaultDoc, `evil!`)).not.toContain(''); }); }); @@ -106,7 +106,7 @@ import {sanitizeHtml} from '../../src/sanitization/html_sanitizer'; for (const attr of dangerousAttrs) { it(`${attr}`, () => { - expect(sanitizeHtml(defaultDoc, `evil!`)).toEqual('evil!'); + expect(_sanitizeHtml(defaultDoc, `evil!`)).toEqual('evil!'); }); } }); @@ -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, '
'); + _sanitizeHtml(defaultDoc, '
'); } catch (e) { // depending on the browser, we might ge an exception } try { - sanitizeHtml(defaultDoc, '
'); + _sanitizeHtml(defaultDoc, '
'); } catch (e) { // depending on the browser, we might ge an exception } try { - sanitizeHtml(defaultDoc, '
'); + _sanitizeHtml( + defaultDoc, '
'); } 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, ''); + const output = _sanitizeHtml(defaultDoc, ''); 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, '

')) .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, 'CLICKME')) + expect(_sanitizeHtml(defaultDoc, 'CLICKME')) .toMatch(/CLICKME<\/a>/); }); } diff --git a/packages/core/test/sanitization/sanatization_spec.ts b/packages/core/test/sanitization/sanatization_spec.ts new file mode 100644 index 0000000000..6c9f31e8c0 --- /dev/null +++ b/packages/core/test/sanitization/sanatization_spec.ts @@ -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('

')).toEqual('
'); + expect(sanitizeHtml(new Wrap('
'))).toEqual('
'); + expect(sanitizeHtml('')) + .toEqual(''); + expect(sanitizeHtml(new Wrap(''))) + .toEqual(''); + expect(sanitizeHtml(bypassSanitizationTrustUrl(''))) + .toEqual(''); + expect(sanitizeHtml(bypassSanitizationTrustHtml(''))) + .toEqual(''); + }); + + 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'); + }); +}); \ No newline at end of file diff --git a/packages/core/test/sanitization/style_sanitizer_spec.ts b/packages/core/test/sanitization/style_sanitizer_spec.ts index f50beeff4a..5adafceb8e 100644 --- a/packages/core/test/sanitization/style_sanitizer_spec.ts +++ b/packages/core/test/sanitization/style_sanitizer_spec.ts @@ -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(''); diff --git a/packages/core/test/sanitization/url_sanitizer_spec.ts b/packages/core/test/sanitization/url_sanitizer_spec.ts index bfa5180b08..69c1f705a4 100644 --- a/packages/core/test/sanitization/url_sanitizer_spec.ts +++ b/packages/core/test/sanitization/url_sanitizer_spec.ts @@ -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:/)); } }); diff --git a/packages/platform-browser/src/security/dom_sanitization_service.ts b/packages/platform-browser/src/security/dom_sanitization_service.ts index 06a11e13ae..13427ca295 100644 --- a/packages/platform-browser/src/security/dom_sanitization_service.ts +++ b/packages/platform-browser/src/security/dom_sanitization_service.ts @@ -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;