refactor(core): move sanitization into core (#22540)

This is in preparation of having Ivy have sanitization inline.

PR Close #22540
This commit is contained in:
Miško Hevery 2018-03-01 13:16:13 -08:00 committed by Kara Erickson
parent 065bcc5aad
commit 538f1d980f
17 changed files with 108 additions and 81 deletions

View File

@ -33,7 +33,7 @@ export {EventEmitter} from './event_emitter';
export {ErrorHandler} from './error_handler'; export {ErrorHandler} from './error_handler';
export * from './core_private_export'; export * from './core_private_export';
export * from './core_render3_private_export'; export * from './core_render3_private_export';
export {Sanitizer, SecurityContext} from './security'; export {Sanitizer, SecurityContext} from './sanitization/security';
export * from './codegen_private_exports'; export * from './codegen_private_exports';
export * from './animation/animation_metadata_wrapped'; export * from './animation/animation_metadata_wrapped';
import {AnimationTriggerMetadata} from './animation/animation_metadata_wrapped'; import {AnimationTriggerMetadata} from './animation/animation_metadata_wrapped';

View File

@ -18,6 +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 {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 {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

@ -72,3 +72,7 @@ export {
Pp as ɵPp, Pp as ɵPp,
} from './render3/index'; } from './render3/index';
// 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

@ -8,8 +8,6 @@
import {isDevMode} from '@angular/core'; import {isDevMode} from '@angular/core';
import {DomAdapter, getDOM} from '../dom/dom_adapter';
import {InertBodyHelper} from './inert_body'; import {InertBodyHelper} from './inert_body';
import {sanitizeSrcset, sanitizeUrl} from './url_sanitizer'; import {sanitizeSrcset, sanitizeUrl} from './url_sanitizer';
@ -95,58 +93,61 @@ class SanitizingHtmlSerializer {
// because characters were re-encoded. // because characters were re-encoded.
public sanitizedSomething = false; public sanitizedSomething = false;
private buf: string[] = []; private buf: string[] = [];
private DOM = getDOM();
sanitizeChildren(el: Element): string { sanitizeChildren(el: Element): string {
// This cannot use a TreeWalker, as it has to run on Angular's various DOM adapters. // This cannot use a TreeWalker, as it has to run on Angular's various DOM adapters.
// However this code never accesses properties off of `document` before deleting its contents // However this code never accesses properties off of `document` before deleting its contents
// again, so it shouldn't be vulnerable to DOM clobbering. // again, so it shouldn't be vulnerable to DOM clobbering.
let current: Node = this.DOM.firstChild(el) !; let current: Node = el.firstChild !;
while (current) { while (current) {
if (this.DOM.isElementNode(current)) { if (current.nodeType === Node.ELEMENT_NODE) {
this.startElement(current as Element); this.startElement(current as Element);
} else if (this.DOM.isTextNode(current)) { } else if (current.nodeType === Node.TEXT_NODE) {
this.chars(this.DOM.nodeValue(current) !); this.chars(current.nodeValue !);
} else { } else {
// Strip non-element, non-text nodes. // Strip non-element, non-text nodes.
this.sanitizedSomething = true; this.sanitizedSomething = true;
} }
if (this.DOM.firstChild(current)) { if (current.firstChild) {
current = this.DOM.firstChild(current) !; current = current.firstChild !;
continue; continue;
} }
while (current) { while (current) {
// Leaving the element. Walk up and to the right, closing tags as we go. // Leaving the element. Walk up and to the right, closing tags as we go.
if (this.DOM.isElementNode(current)) { if (current.nodeType === Node.ELEMENT_NODE) {
this.endElement(current as Element); this.endElement(current as Element);
} }
let next = this.checkClobberedElement(current, this.DOM.nextSibling(current) !); let next = this.checkClobberedElement(current, current.nextSibling !);
if (next) { if (next) {
current = next; current = next;
break; break;
} }
current = this.checkClobberedElement(current, this.DOM.parentElement(current) !); current = this.checkClobberedElement(current, current.parentNode !);
} }
} }
return this.buf.join(''); return this.buf.join('');
} }
private startElement(element: Element) { private startElement(element: Element) {
const tagName = this.DOM.nodeName(element).toLowerCase(); const tagName = element.nodeName.toLowerCase();
if (!VALID_ELEMENTS.hasOwnProperty(tagName)) { if (!VALID_ELEMENTS.hasOwnProperty(tagName)) {
this.sanitizedSomething = true; this.sanitizedSomething = true;
return; return;
} }
this.buf.push('<'); this.buf.push('<');
this.buf.push(tagName); this.buf.push(tagName);
this.DOM.attributeMap(element).forEach((value: string, attrName: string) => { const elAttrs = element.attributes;
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(); const lower = attrName.toLowerCase();
if (!VALID_ATTRS.hasOwnProperty(lower)) { if (!VALID_ATTRS.hasOwnProperty(lower)) {
this.sanitizedSomething = true; this.sanitizedSomething = true;
return; continue;
} }
// 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);
@ -156,12 +157,12 @@ class SanitizingHtmlSerializer {
this.buf.push('="'); this.buf.push('="');
this.buf.push(encodeEntities(value)); this.buf.push(encodeEntities(value));
this.buf.push('"'); this.buf.push('"');
}); };
this.buf.push('>'); this.buf.push('>');
} }
private endElement(current: Element) { private endElement(current: Element) {
const tagName = this.DOM.nodeName(current).toLowerCase(); const tagName = current.nodeName.toLowerCase();
if (VALID_ELEMENTS.hasOwnProperty(tagName) && !VOID_ELEMENTS.hasOwnProperty(tagName)) { if (VALID_ELEMENTS.hasOwnProperty(tagName) && !VOID_ELEMENTS.hasOwnProperty(tagName)) {
this.buf.push('</'); this.buf.push('</');
this.buf.push(tagName); this.buf.push(tagName);
@ -172,9 +173,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 && this.DOM.contains(node, nextNode)) { if (nextNode && node.contains(nextNode)) {
throw new Error( throw new Error(
`Failed to sanitize html because the element is clobbered: ${this.DOM.getOuterHTML(node)}`); `Failed to sanitize html because the element is clobbered: ${(node as Element).outerHTML}`);
} }
return nextNode; return nextNode;
} }
@ -214,10 +215,9 @@ let inertBodyHelper: InertBodyHelper;
* 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 {
const DOM = getDOM();
let inertBodyElement: HTMLElement|null = null; let inertBodyElement: HTMLElement|null = null;
try { try {
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc, DOM); inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc);
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime). // Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : ''; let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml); inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
@ -234,25 +234,33 @@ export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
mXSSAttempts--; mXSSAttempts--;
unsafeHtml = parsedHtml; unsafeHtml = parsedHtml;
parsedHtml = DOM.getInnerHTML(inertBodyElement); parsedHtml = inertBodyElement !.innerHTML;
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml); inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
} while (unsafeHtml !== parsedHtml); } while (unsafeHtml !== parsedHtml);
const sanitizer = new SanitizingHtmlSerializer(); const sanitizer = new SanitizingHtmlSerializer();
const safeHtml = const safeHtml = sanitizer.sanitizeChildren(
sanitizer.sanitizeChildren(DOM.getTemplateContent(inertBodyElement) || inertBodyElement); getTemplateContent(inertBodyElement !) as Element || inertBodyElement);
if (isDevMode() && sanitizer.sanitizedSomething) { if (isDevMode() && sanitizer.sanitizedSomething) {
DOM.log('WARNING: sanitizing HTML stripped some content (see http://g.co/ng/security#xss).'); console.warn(
'WARNING: sanitizing HTML stripped some content (see http://g.co/ng/security#xss).');
} }
return safeHtml; return safeHtml;
} finally { } finally {
// In case anything goes wrong, clear out inertElement to reset the entire DOM structure. // In case anything goes wrong, clear out inertElement to reset the entire DOM structure.
if (inertBodyElement) { if (inertBodyElement) {
const parent = DOM.getTemplateContent(inertBodyElement) || inertBodyElement; const parent = getTemplateContent(inertBodyElement) || inertBodyElement;
for (const child of DOM.childNodesAsList(parent)) { while (parent.firstChild) {
DOM.removeChild(parent, child); parent.removeChild(parent.firstChild);
} }
} }
} }
} }
function getTemplateContent(el: Node): Node|null {
return 'content' in el && isTemplateElement(el) ? (<any>el).content : null;
}
function isTemplateElement(el: Node): boolean {
return el.nodeType === Node.ELEMENT_NODE && el.nodeName === 'TEMPLATE';
}

View File

@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {DomAdapter, getDOM} from '../dom/dom_adapter';
/** /**
* This helper class is used to get hold of an inert tree of DOM elements containing dirty HTML * This helper class is used to get hold of an inert tree of DOM elements containing dirty HTML
* that needs sanitizing. * that needs sanitizing.
@ -18,22 +16,22 @@ import {DomAdapter, getDOM} from '../dom/dom_adapter';
*/ */
export class InertBodyHelper { export class InertBodyHelper {
private inertBodyElement: HTMLElement; private inertBodyElement: HTMLElement;
private inertDocument: Document;
constructor(private defaultDoc: any, private DOM: DomAdapter) { constructor(private defaultDoc: Document) {
const inertDocument = this.DOM.createHtmlDocument(); this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
this.inertBodyElement = inertDocument.body; this.inertBodyElement = this.inertDocument.body;
if (this.inertBodyElement == null) { if (this.inertBodyElement == null) {
// usually there should be only one body element in the document, but IE doesn't have any, so // usually there should be only one body element in the document, but IE doesn't have any, so
// we need to create one. // we need to create one.
const inertHtml = this.DOM.createElement('html', inertDocument); const inertHtml = this.inertDocument.createElement('html');
this.inertBodyElement = this.DOM.createElement('body', inertDocument); this.inertDocument.appendChild(inertHtml);
this.DOM.appendChild(inertHtml, this.inertBodyElement); this.inertBodyElement = this.inertDocument.createElement('body');
this.DOM.appendChild(inertDocument, inertHtml); inertHtml.appendChild(this.inertBodyElement);
} }
this.DOM.setInnerHTML( this.inertBodyElement.innerHTML = '<svg><g onload="this.parentNode.remove()"></g></svg>';
this.inertBodyElement, '<svg><g onload="this.parentNode.remove()"></g></svg>');
if (this.inertBodyElement.querySelector && !this.inertBodyElement.querySelector('svg')) { if (this.inertBodyElement.querySelector && !this.inertBodyElement.querySelector('svg')) {
// We just hit the Safari 10.1 bug - which allows JS to run inside the SVG G element // We just hit the Safari 10.1 bug - which allows JS to run inside the SVG G element
// so use the XHR strategy. // so use the XHR strategy.
@ -41,8 +39,8 @@ export class InertBodyHelper {
return; return;
} }
this.DOM.setInnerHTML( this.inertBodyElement.innerHTML =
this.inertBodyElement, '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">'); '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">';
if (this.inertBodyElement.querySelector && this.inertBodyElement.querySelector('svg img')) { if (this.inertBodyElement.querySelector && this.inertBodyElement.querySelector('svg img')) {
// We just hit the Firefox bug - which prevents the inner img JS from being sanitized // We just hit the Firefox bug - which prevents the inner img JS from being sanitized
// so use the DOMParser strategy, if it is available. // so use the DOMParser strategy, if it is available.
@ -118,17 +116,17 @@ export class InertBodyHelper {
*/ */
private getInertBodyElement_InertDocument(html: string) { private getInertBodyElement_InertDocument(html: string) {
// Prefer using <template> element if supported. // Prefer using <template> element if supported.
const templateEl = this.DOM.createElement('template'); const templateEl = this.inertDocument.createElement('template');
if ('content' in templateEl) { if ('content' in templateEl) {
this.DOM.setInnerHTML(templateEl, html); templateEl.innerHTML = html;
return templateEl; return templateEl;
} }
this.DOM.setInnerHTML(this.inertBodyElement, html); this.inertBodyElement.innerHTML = html;
// Support: IE 9-11 only // Support: IE 9-11 only
// strip custom-namespaced attributes on IE<=11 // strip custom-namespaced attributes on IE<=11
if (this.defaultDoc.documentMode) { if ((this.defaultDoc as any).documentMode) {
this.stripCustomNsAttrs(this.inertBodyElement); this.stripCustomNsAttrs(this.inertBodyElement);
} }
@ -144,13 +142,19 @@ export class InertBodyHelper {
* strips them all. * strips them all.
*/ */
private stripCustomNsAttrs(el: Element) { private stripCustomNsAttrs(el: Element) {
this.DOM.attributeMap(el).forEach((_, attrName) => { const elAttrs = el.attributes;
// loop backwards so that we can support removals.
for (let i = elAttrs.length - 1; 0 < i; i--) {
const attrib = elAttrs.item(i);
const attrName = attrib.name;
if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) { if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
this.DOM.removeAttribute(el, attrName); el.removeAttribute(attrName);
} }
}); }
for (const n of this.DOM.childNodesAsList(el)) { let childNode = el.firstChild;
if (this.DOM.isElementNode(n)) this.stripCustomNsAttrs(n as Element); while (childNode) {
if (childNode.nodeType === Node.ELEMENT_NODE) this.stripCustomNsAttrs(childNode as Element);
childNode = childNode.nextSibling;
} }
} }
} }

View File

@ -0,0 +1,10 @@
# Sanitization
This folder contains sanitization related code.
## History
It used to be that sanitization related code used to be in `@angular/platform-browser` since it is platform related. While this is true, in practice the compiler schema is permanently tied to the DOM and hence the fact that sanitizer could in theory be replaced is not used in practice.
In order to better support tree shaking we need to be able to refer to the sanitization functions from the Ivy code. For this reason the code has been moved into the `@angular/core`.

View File

@ -8,8 +8,6 @@
import {isDevMode} from '@angular/core'; import {isDevMode} from '@angular/core';
import {getDOM} from '../dom/dom_adapter';
import {sanitizeUrl} from './url_sanitizer'; import {sanitizeUrl} from './url_sanitizer';
@ -98,7 +96,7 @@ export function sanitizeStyle(value: string): string {
} }
if (isDevMode()) { if (isDevMode()) {
getDOM().log( console.warn(
`WARNING: sanitizing unsafe style value ${value} (see http://g.co/ng/security#xss).`); `WARNING: sanitizing unsafe style value ${value} (see http://g.co/ng/security#xss).`);
} }

View File

@ -8,8 +8,6 @@
import {isDevMode} from '@angular/core'; import {isDevMode} from '@angular/core';
import {getDOM} from '../dom/dom_adapter';
/** /**
* 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.
@ -51,7 +49,7 @@ export function sanitizeUrl(url: string): string {
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;
if (isDevMode()) { if (isDevMode()) {
getDOM().log(`WARNING: sanitizing unsafe URL value ${url} (see http://g.co/ng/security#xss)`); console.warn(`WARNING: sanitizing unsafe URL value ${url} (see http://g.co/ng/security#xss)`);
} }
return 'unsafe:' + url; return 'unsafe:' + url;

View File

@ -7,7 +7,7 @@
*/ */
import {RendererType2} from '../render/api'; import {RendererType2} from '../render/api';
import {SecurityContext} from '../security'; import {SecurityContext} from '../sanitization/security';
import {BindingDef, BindingFlags, ElementData, ElementHandleEventFn, NodeDef, NodeFlags, OutputDef, OutputType, QueryValueType, ViewData, ViewDefinitionFactory, asElementData} from './types'; import {BindingDef, BindingFlags, ElementData, ElementHandleEventFn, NodeDef, NodeFlags, OutputDef, OutputType, QueryValueType, ViewData, ViewDefinitionFactory, asElementData} from './types';
import {NOOP, calcBindingFlags, checkAndUpdateBinding, dispatchEvent, elementEventFullName, getParentRenderElement, resolveDefinition, resolveRendererType2, splitMatchedQueriesDsl, splitNamespace} from './util'; import {NOOP, calcBindingFlags, checkAndUpdateBinding, dispatchEvent, elementEventFullName, getParentRenderElement, resolveDefinition, resolveRendererType2, splitMatchedQueriesDsl, splitNamespace} from './util';

View File

@ -13,7 +13,7 @@ import {ErrorHandler} from '../error_handler';
import {ComponentFactory} from '../linker/component_factory'; import {ComponentFactory} from '../linker/component_factory';
import {NgModuleRef} from '../linker/ng_module_factory'; import {NgModuleRef} from '../linker/ng_module_factory';
import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api'; import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api';
import {Sanitizer} from '../security'; import {Sanitizer} from '../sanitization/security';
import {Type} from '../type'; import {Type} from '../type';
import {isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './errors'; import {isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './errors';

View File

@ -14,7 +14,7 @@ import {QueryList} from '../linker/query_list';
import {TemplateRef} from '../linker/template_ref'; import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref'; import {ViewContainerRef} from '../linker/view_container_ref';
import {Renderer2, RendererFactory2, RendererType2} from '../render/api'; import {Renderer2, RendererFactory2, RendererType2} from '../render/api';
import {Sanitizer, SecurityContext} from '../security'; import {Sanitizer, SecurityContext} from '../sanitization/security';
import {Type} from '../type'; import {Type} from '../type';

View File

@ -8,8 +8,7 @@
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {getDOM} from '../../src/dom/dom_adapter'; import {sanitizeHtml} from '../../src/sanitization/html_sanitizer';
import {sanitizeHtml} from '../../src/security/html_sanitizer';
{ {
describe('HTML sanitizer', () => { describe('HTML sanitizer', () => {
@ -18,13 +17,13 @@ import {sanitizeHtml} from '../../src/security/html_sanitizer';
let logMsgs: string[]; let logMsgs: string[];
beforeEach(() => { beforeEach(() => {
defaultDoc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument(); defaultDoc = document;
logMsgs = []; logMsgs = [];
originalLog = getDOM().log; // Monkey patch DOM.log. originalLog = console.warn; // Monkey patch DOM.log.
getDOM().log = (msg) => logMsgs.push(msg); console.warn = (msg: any) => logMsgs.push(msg);
}); });
afterEach(() => { getDOM().log = 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>'))

View File

@ -8,8 +8,7 @@
import * as t from '@angular/core/testing/src/testing_internal'; import * as t from '@angular/core/testing/src/testing_internal';
import {getDOM} from '../../src/dom/dom_adapter'; import {sanitizeStyle} from '../../src/sanitization/style_sanitizer';
import {sanitizeStyle} from '../../src/security/style_sanitizer';
{ {
t.describe('Style sanitizer', () => { t.describe('Style sanitizer', () => {
@ -18,10 +17,11 @@ import {sanitizeStyle} from '../../src/security/style_sanitizer';
t.beforeEach(() => { t.beforeEach(() => {
logMsgs = []; logMsgs = [];
originalLog = getDOM().log; // Monkey patch DOM.log. originalLog = console.warn; // Monkey patch DOM.log.
getDOM().log = (msg) => logMsgs.push(msg); console.warn = (msg: any) => logMsgs.push(msg);
}); });
t.afterEach(() => { getDOM().log = originalLog; });
afterEach(() => { console.warn = originalLog; });
function expectSanitize(v: string) { return t.expect(sanitizeStyle(v)); } function expectSanitize(v: string) { return t.expect(sanitizeStyle(v)); }

View File

@ -8,8 +8,7 @@
import * as t from '@angular/core/testing/src/testing_internal'; import * as t from '@angular/core/testing/src/testing_internal';
import {getDOM} from '../../src/dom/dom_adapter'; import {sanitizeSrcset, sanitizeUrl} from '../../src/sanitization/url_sanitizer';
import {sanitizeSrcset, sanitizeUrl} from '../../src/security/url_sanitizer';
{ {
t.describe('URL sanitizer', () => { t.describe('URL sanitizer', () => {
@ -18,10 +17,11 @@ import {sanitizeSrcset, sanitizeUrl} from '../../src/security/url_sanitizer';
t.beforeEach(() => { t.beforeEach(() => {
logMsgs = []; logMsgs = [];
originalLog = getDOM().log; // Monkey patch DOM.log. originalLog = console.warn; // Monkey patch DOM.log.
getDOM().log = (msg) => logMsgs.push(msg); console.warn = (msg: any) => logMsgs.push(msg);
}); });
t.afterEach(() => { getDOM().log = 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()');

View File

@ -53,6 +53,7 @@ export function withBody<T>(html: string, blockFn: T): T {
let savedDocument: Document|undefined = undefined; let savedDocument: Document|undefined = undefined;
let savedRequestAnimationFrame: ((callback: FrameRequestCallback) => number)|undefined = undefined; let savedRequestAnimationFrame: ((callback: FrameRequestCallback) => number)|undefined = undefined;
let savedNode: typeof Node|undefined = undefined;
let requestAnimationFrameCount = 0; let requestAnimationFrameCount = 0;
/** /**
@ -87,6 +88,8 @@ export function ensureDocument(): void {
// It fails with Domino with TypeError: Cannot assign to read only property // It fails with Domino with TypeError: Cannot assign to read only property
// 'stopImmediatePropagation' of object '#<Event>' // 'stopImmediatePropagation' of object '#<Event>'
(global as any).Event = null; (global as any).Event = null;
savedNode = (global as any).Node;
(global as any).Node = domino.impl.Node;
savedRequestAnimationFrame = (global as any).requestAnimationFrame; savedRequestAnimationFrame = (global as any).requestAnimationFrame;
(global as any).requestAnimationFrame = function(cb: FrameRequestCallback): number { (global as any).requestAnimationFrame = function(cb: FrameRequestCallback): number {
@ -105,6 +108,10 @@ export function cleanupDocument(): void {
(global as any).document = savedDocument; (global as any).document = savedDocument;
savedDocument = undefined; savedDocument = undefined;
} }
if (savedNode) {
(global as any).Node = savedNode;
savedNode = undefined;
}
if (savedRequestAnimationFrame) { if (savedRequestAnimationFrame) {
(global as any).requestAnimationFrame = savedRequestAnimationFrame; (global as any).requestAnimationFrame = savedRequestAnimationFrame;
savedRequestAnimationFrame = undefined; savedRequestAnimationFrame = undefined;

View File

@ -6,14 +6,10 @@
* 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} 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';
import {sanitizeHtml} from './html_sanitizer';
import {sanitizeStyle} from './style_sanitizer';
import {sanitizeUrl} from './url_sanitizer';
export {SecurityContext}; export {SecurityContext};