diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index c4f53ac9a9..a0cc5a4602 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -11,6 +11,7 @@ import {Type} from '../core'; import {Injector} from '../di/injector'; import {ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory'; +import {Sanitizer} from '../sanitization/security'; import {assertComponentType, assertNotNull} from './assert'; import {queueInitHooks, queueLifecycleHooks} from './hooks'; @@ -29,6 +30,9 @@ export interface CreateComponentOptions { /** Which renderer factory to use. */ rendererFactory?: RendererFactory3; + /** A custom sanitizer instance */ + sanitizer?: Sanitizer; + /** * Host element on which the component will be bootstrapped. If not specified, * the component definition's `tag` is used to query the existing DOM for the @@ -120,6 +124,7 @@ export function renderComponent( opts: CreateComponentOptions = {}): T { ngDevMode && assertComponentType(componentType); const rendererFactory = opts.rendererFactory || domRendererFactory3; + const sanitizer = opts.sanitizer || null; const componentDef = (componentType as ComponentType).ngComponentDef as ComponentDef; if (componentDef.type != componentType) componentDef.type = componentType; let component: T; @@ -144,7 +149,7 @@ export function renderComponent( if (rendererFactory.begin) rendererFactory.begin(); // Create element node at index 0 in data array - elementNode = hostElement(componentTag, hostNode, componentDef); + elementNode = hostElement(componentTag, hostNode, componentDef, sanitizer); // Create directive instance with factory() and store at index 0 in directives array component = rootContext.component = diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 86b4ea07ea..8f2162cca5 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -25,6 +25,7 @@ import {isDifferent, stringify} from './util'; import {executeHooks, queueLifecycleHooks, queueInitHooks, executeInitHooks} from './hooks'; import {ViewRef} from './view_ref'; import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; +import {Sanitizer} from '../sanitization/security'; /** * Directive (D) sets a property on all component instances using this constant as a key and the @@ -42,7 +43,7 @@ const _CLEAN_PROMISE = Promise.resolve(null); /** * Function used to sanitize the value before writing it into the renderer. */ -export type Sanitizer = (value: any) => string; +export type SanitizerFn = (value: any) => string; /** * Directive and element indices for top-level directive. @@ -84,6 +85,10 @@ export function getRenderer(): Renderer3 { return renderer; } +export function getCurrentSanitizer(): Sanitizer|null { + return currentView && currentView.sanitizer; +} + /** Used to set the parent property when nodes are created. */ let previousOrParentNode: LNode; @@ -298,7 +303,7 @@ export function executeInitAndContentHooks(): void { export function createLView( viewId: number, renderer: Renderer3, tView: TView, template: ComponentTemplate| null, - context: T | null, flags: LViewFlags): LView { + context: T | null, flags: LViewFlags, sanitizer?: Sanitizer | null): LView { const newView = { parent: currentView, id: viewId, // -1 for component views @@ -320,6 +325,7 @@ export function createLView( lifecycleStage: LifecycleStage.Init, queries: null, injector: currentView && currentView.injector, + sanitizer: sanitizer || null }; return newView; @@ -450,8 +456,8 @@ function resetApplicationState() { export function renderTemplate( hostNode: RElement, template: ComponentTemplate, context: T, providedRendererFactory: RendererFactory3, host: LElementNode | null, - directives?: DirectiveDefListOrFactory | null, - pipes?: PipeDefListOrFactory | null): LElementNode { + directives?: DirectiveDefListOrFactory | null, pipes?: PipeDefListOrFactory | null, + sanitizer?: Sanitizer | null): LElementNode { if (host == null) { resetApplicationState(); rendererFactory = providedRendererFactory; @@ -460,7 +466,7 @@ export function renderTemplate( null, LNodeType.Element, hostNode, createLView( -1, providedRendererFactory.createRenderer(null, null), tView, null, {}, - LViewFlags.CheckAlways)); + LViewFlags.CheckAlways, sanitizer)); } const hostView = host.data !; ngDevMode && assertNotNull(hostView, 'Host node should have an LView defined in host.data.'); @@ -491,7 +497,8 @@ export function renderEmbeddedTemplate( previousOrParentNode = null !; if (viewNode == null) { - const lView = createLView(-1, renderer, tView, template, context, LViewFlags.CheckAlways); + const lView = createLView( + -1, renderer, tView, template, context, LViewFlags.CheckAlways, getCurrentSanitizer()); viewNode = createLNode(null, LNodeType.View, null, lView); rf = RenderFlags.Create; @@ -859,13 +866,14 @@ export function locateHostElement( * @returns LElementNode created */ export function hostElement( - tag: string, rNode: RElement | null, def: ComponentDef): LElementNode { + tag: string, rNode: RElement | null, def: ComponentDef, + sanitizer?: Sanitizer | null): LElementNode { resetApplicationState(); const node = createLNode( 0, LNodeType.Element, rNode, createLView( -1, renderer, getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs), null, null, - def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways)); + def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, sanitizer)); if (firstTemplatePass) { node.tNode = createTNode(tag as string, null, null); @@ -958,7 +966,7 @@ export function elementEnd() { * @param sanitizer An optional function used to sanitize the value. */ export function elementAttribute( - index: number, name: string, value: any, sanitizer?: Sanitizer): void { + index: number, name: string, value: any, sanitizer?: SanitizerFn): void { if (value !== NO_CHANGE) { const element: LElementNode = data[index]; if (value == null) { @@ -989,7 +997,7 @@ export function elementAttribute( */ export function elementProperty( - index: number, propName: string, value: T | NO_CHANGE, sanitizer?: Sanitizer): void { + index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): void { if (value === NO_CHANGE) return; const node = data[index] as LElementNode; const tNode = node.tNode !; @@ -1152,10 +1160,10 @@ export function elementClass(index: number, value: T | NO_CHANGE): void { export function elementStyleNamed( index: number, styleName: string, value: T | NO_CHANGE, suffix?: string): void; export function elementStyleNamed( - index: number, styleName: string, value: T | NO_CHANGE, sanitizer?: Sanitizer): void; + index: number, styleName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): void; export function elementStyleNamed( index: number, styleName: string, value: T | NO_CHANGE, - suffixOrSanitizer?: string | Sanitizer): void { + suffixOrSanitizer?: string | SanitizerFn): void { if (value !== NO_CHANGE) { const lElement: LElementNode = data[index]; if (value == null) { @@ -1305,7 +1313,8 @@ function addComponentLogic(index: number, instance: T, def: ComponentDef): currentView, createLView( -1, rendererFactory.createRenderer( previousOrParentNode.native as RElement, def.rendererType), - tView, null, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways)); + tView, null, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, + getCurrentSanitizer())); (previousOrParentNode.data as any) = hostView; (hostView.node as any) = previousOrParentNode; @@ -1596,7 +1605,7 @@ export function embeddedViewStart(viewBlockId: number): RenderFlags { // When we create a new LView, we always reset the state of the instructions. const newView = createLView( viewBlockId, renderer, getOrCreateEmbeddedTView(viewBlockId, container), null, null, - LViewFlags.CheckAlways); + LViewFlags.CheckAlways, getCurrentSanitizer()); if (lContainer.queries) { newView.queries = lContainer.queries.enterView(lContainer.nextIndex); } diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 24742998ee..1e412140b2 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -7,6 +7,8 @@ */ import {Injector} from '../../di/injector'; +import {Sanitizer} from '../../sanitization/security'; + import {LContainer} from './container'; import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDef, PipeDefList} from './definition'; import {LElementNode, LViewNode, TNode} from './node'; @@ -195,6 +197,11 @@ export interface LView { * An optional Module Injector to be used as fall back after Element Injectors are consulted. */ injector: Injector|null; + + /** + * An optional custom sanitizer + */ + sanitizer: Sanitizer|null; } /** Flags associated with an LView (saved in LView.flags) */ diff --git a/packages/core/src/sanitization/sanitization.ts b/packages/core/src/sanitization/sanitization.ts index 2f647fe640..ecd0203456 100644 --- a/packages/core/src/sanitization/sanitization.ts +++ b/packages/core/src/sanitization/sanitization.ts @@ -6,9 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import {getCurrentSanitizer} from '../render3/instructions'; import {stringify} from '../render3/util'; import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer'; +import {SecurityContext} from './security'; import {_sanitizeStyle as _sanitizeStyle} from './style_sanitizer'; import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer'; @@ -79,6 +81,10 @@ export interface TrustedResourceUrlString extends TrustedString { * and urls have been removed. */ export function sanitizeHtml(unsafeHtml: any): string { + const s = getCurrentSanitizer(); + if (s) { + return s.sanitize(SecurityContext.HTML, unsafeHtml) || ''; + } if (unsafeHtml instanceof String && (unsafeHtml as TrustedHtmlString)[BRAND] === 'Html') { return unsafeHtml.toString(); } @@ -99,6 +105,10 @@ export function sanitizeHtml(unsafeHtml: any): string { * dangerous javascript and urls have been removed. */ export function sanitizeStyle(unsafeStyle: any): string { + const s = getCurrentSanitizer(); + if (s) { + return s.sanitize(SecurityContext.STYLE, unsafeStyle) || ''; + } if (unsafeStyle instanceof String && (unsafeStyle as TrustedStyleString)[BRAND] === 'Style') { return unsafeStyle.toString(); } @@ -120,6 +130,10 @@ export function sanitizeStyle(unsafeStyle: any): string { * all of the dangerous javascript has been removed. */ export function sanitizeUrl(unsafeUrl: any): string { + const s = getCurrentSanitizer(); + if (s) { + return s.sanitize(SecurityContext.URL, unsafeUrl) || ''; + } if (unsafeUrl instanceof String && (unsafeUrl as TrustedUrlString)[BRAND] === 'Url') { return unsafeUrl.toString(); } @@ -136,6 +150,10 @@ export function sanitizeUrl(unsafeUrl: any): string { * only trusted `url`s have been allowed to pass. */ export function sanitizeResourceUrl(unsafeResourceUrl: any): string { + const s = getCurrentSanitizer(); + if (s) { + return s.sanitize(SecurityContext.RESOURCE_URL, unsafeResourceUrl) || ''; + } if (unsafeResourceUrl instanceof String && (unsafeResourceUrl as TrustedResourceUrlString)[BRAND] === 'ResourceUrl') { return unsafeResourceUrl.toString(); @@ -153,6 +171,10 @@ export function sanitizeResourceUrl(unsafeResourceUrl: any): string { * because only trusted `scripts`s have been allowed to pass. */ export function sanitizeScript(unsafeScript: any): string { + const s = getCurrentSanitizer(); + if (s) { + return s.sanitize(SecurityContext.SCRIPT, unsafeScript) || ''; + } if (unsafeScript instanceof String && (unsafeScript as TrustedScriptString)[BRAND] === 'Script') { return unsafeScript.toString(); } diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index a0ad181105..63f67c0bda 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -101,6 +101,9 @@ { "name": "firstTemplatePass" }, + { + "name": "getCurrentSanitizer" + }, { "name": "getDirectiveInstance" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index b45915deb6..d9f7140cfe 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -383,6 +383,9 @@ { "name": "generatePropertyAliases" }, + { + "name": "getCurrentSanitizer" + }, { "name": "getDirectiveInstance" }, diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index 224b17c436..18be0225ca 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -13,17 +13,28 @@ import {defineComponent} from '../../src/render3/definition'; import {bind, container, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleNamed, interpolation1, renderTemplate, text, textBinding} 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 {TrustedString, bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization'; +import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; import {NgForOf} from './common_with_def'; import {ComponentFixture, TemplateFixture} from './render_util'; describe('instructions', () => { + function createAnchor() { + elementStart(0, 'a'); + elementEnd(); + } + function createDiv() { elementStart(0, 'div'); elementEnd(); } + function createScript() { + elementStart(0, 'script'); + elementEnd(); + } + describe('elementAttribute', () => { it('should use sanitizer function', () => { const t = new TemplateFixture(createDiv); @@ -177,4 +188,210 @@ describe('instructions', () => { }); }); + + describe('sanitization injection compatibility', () => { + it('should work for url sanitization', () => { + const s = new LocalMockSanitizer(value => `${value}-sanitized`); + const t = new TemplateFixture(createAnchor, undefined, null, null, s); + const inputValue = 'http://foo'; + const outputValue = 'http://foo-sanitized'; + + t.update(() => elementAttribute(0, 'href', inputValue, sanitizeUrl)); + expect(t.html).toEqual(``); + expect(s.lastSanitizedValue).toEqual(outputValue); + }); + + it('should bypass url sanitization if marked by the service', () => { + const s = new LocalMockSanitizer(value => ''); + const t = new TemplateFixture(createAnchor, undefined, null, null, s); + const inputValue = s.bypassSecurityTrustUrl('http://foo'); + const outputValue = 'http://foo'; + + t.update(() => elementAttribute(0, 'href', inputValue, sanitizeUrl)); + expect(t.html).toEqual(``); + expect(s.lastSanitizedValue).toBeFalsy(); + }); + + it('should bypass ivy-level url sanitization if a custom sanitizer is used', () => { + const s = new LocalMockSanitizer(value => ''); + const t = new TemplateFixture(createAnchor, undefined, null, null, s); + const inputValue = bypassSanitizationTrustUrl('http://foo'); + const outputValue = 'http://foo-ivy'; + + t.update(() => elementAttribute(0, 'href', inputValue, sanitizeUrl)); + expect(t.html).toEqual(``); + expect(s.lastSanitizedValue).toBeFalsy(); + }); + + it('should work for style sanitization', () => { + const s = new LocalMockSanitizer(value => `color:blue`); + const t = new TemplateFixture(createDiv, undefined, null, null, s); + const inputValue = 'color:red'; + const outputValue = 'color:blue'; + + t.update(() => elementAttribute(0, 'style', inputValue, sanitizeStyle)); + expect(stripStyleWsCharacters(t.html)).toEqual(`
`); + expect(s.lastSanitizedValue).toEqual(outputValue); + }); + + it('should bypass style sanitization if marked by the service', () => { + const s = new LocalMockSanitizer(value => ''); + const t = new TemplateFixture(createDiv, undefined, null, null, s); + const inputValue = s.bypassSecurityTrustStyle('color:maroon'); + const outputValue = 'color:maroon'; + + t.update(() => elementAttribute(0, 'style', inputValue, sanitizeStyle)); + expect(stripStyleWsCharacters(t.html)).toEqual(`
`); + expect(s.lastSanitizedValue).toBeFalsy(); + }); + + it('should bypass ivy-level style sanitization if a custom sanitizer is used', () => { + const s = new LocalMockSanitizer(value => ''); + const t = new TemplateFixture(createDiv, undefined, null, null, s); + const inputValue = bypassSanitizationTrustStyle('font-family:foo'); + const outputValue = 'font-family:foo-ivy'; + + t.update(() => elementAttribute(0, 'style', inputValue, sanitizeStyle)); + expect(stripStyleWsCharacters(t.html)).toEqual(`
`); + expect(s.lastSanitizedValue).toBeFalsy(); + }); + + it('should work for resourceUrl sanitization', () => { + const s = new LocalMockSanitizer(value => `${value}-sanitized`); + const t = new TemplateFixture(createScript, undefined, null, null, s); + const inputValue = 'http://resource'; + const outputValue = 'http://resource-sanitized'; + + t.update(() => elementAttribute(0, 'src', inputValue, sanitizeResourceUrl)); + expect(t.html).toEqual(``); + expect(s.lastSanitizedValue).toEqual(outputValue); + }); + + it('should bypass resourceUrl sanitization if marked by the service', () => { + const s = new LocalMockSanitizer(value => ''); + const t = new TemplateFixture(createScript, undefined, null, null, s); + const inputValue = s.bypassSecurityTrustResourceUrl('file://all-my-secrets.pdf'); + const outputValue = 'file://all-my-secrets.pdf'; + + t.update(() => elementAttribute(0, 'src', inputValue, sanitizeResourceUrl)); + expect(t.html).toEqual(``); + expect(s.lastSanitizedValue).toBeFalsy(); + }); + + it('should bypass ivy-level resourceUrl sanitization if a custom sanitizer is used', () => { + const s = new LocalMockSanitizer(value => ''); + const t = new TemplateFixture(createScript, undefined, null, null, s); + const inputValue = bypassSanitizationTrustResourceUrl('file://all-my-secrets.pdf'); + const outputValue = 'file://all-my-secrets.pdf-ivy'; + + t.update(() => elementAttribute(0, 'src', inputValue, sanitizeResourceUrl)); + expect(t.html).toEqual(``); + expect(s.lastSanitizedValue).toBeFalsy(); + }); + + it('should work for script sanitization', () => { + const s = new LocalMockSanitizer(value => `${value} //sanitized`); + const t = new TemplateFixture(createScript, undefined, null, null, s); + const inputValue = 'fn();'; + const outputValue = 'fn(); //sanitized'; + + t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeScript)); + expect(t.html).toEqual(``); + expect(s.lastSanitizedValue).toEqual(outputValue); + }); + + it('should bypass script sanitization if marked by the service', () => { + const s = new LocalMockSanitizer(value => ''); + const t = new TemplateFixture(createScript, undefined, null, null, s); + const inputValue = s.bypassSecurityTrustScript('alert("bar")'); + const outputValue = 'alert("bar")'; + + t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeScript)); + expect(t.html).toEqual(``); + expect(s.lastSanitizedValue).toBeFalsy(); + }); + + it('should bypass ivy-level script sanitization if a custom sanitizer is used', () => { + const s = new LocalMockSanitizer(value => ''); + const t = new TemplateFixture(createScript, undefined, null, null, s); + const inputValue = bypassSanitizationTrustScript('alert("bar")'); + const outputValue = 'alert("bar")-ivy'; + + t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeScript)); + expect(t.html).toEqual(``); + expect(s.lastSanitizedValue).toBeFalsy(); + }); + + it('should work for html sanitization', () => { + const s = new LocalMockSanitizer(value => `${value} `); + const t = new TemplateFixture(createDiv, undefined, null, null, s); + const inputValue = '
'; + const outputValue = '
'; + + t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml)); + expect(t.html).toEqual(`
${outputValue}
`); + expect(s.lastSanitizedValue).toEqual(outputValue); + }); + + it('should bypass html sanitization if marked by the service', () => { + const s = new LocalMockSanitizer(value => ''); + const t = new TemplateFixture(createDiv, undefined, null, null, s); + const inputValue = s.bypassSecurityTrustHtml('
'); + const outputValue = '
'; + + t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml)); + expect(t.html).toEqual(`
${outputValue}
`); + expect(s.lastSanitizedValue).toBeFalsy(); + }); + + it('should bypass ivy-level script sanitization if a custom sanitizer is used', () => { + const s = new LocalMockSanitizer(value => ''); + const t = new TemplateFixture(createDiv, undefined, null, null, s); + const inputValue = bypassSanitizationTrustHtml('
'); + const outputValue = '
-ivy'; + + t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml)); + expect(t.html).toEqual(`
${outputValue}
`); + expect(s.lastSanitizedValue).toBeFalsy(); + }); + }); }); + +class LocalSanitizedValue { + constructor(public value: any) {} + + toString() { return this.value; } +} + +class LocalMockSanitizer implements Sanitizer { + public lastSanitizedValue: string|null; + + constructor(private _interceptor: (value: string|null|any) => string) {} + + sanitize(context: SecurityContext, value: LocalSanitizedValue|string|null|any): string|null { + if (value instanceof String) { + return value.toString() + '-ivy'; + } + + if (value instanceof LocalSanitizedValue) { + return value.toString(); + } + + return this.lastSanitizedValue = this._interceptor(value); + } + + bypassSecurityTrustHtml(value: string) { return new LocalSanitizedValue(value); } + + bypassSecurityTrustStyle(value: string) { return new LocalSanitizedValue(value); } + + bypassSecurityTrustScript(value: string) { return new LocalSanitizedValue(value); } + + bypassSecurityTrustUrl(value: string) { return new LocalSanitizedValue(value); } + + bypassSecurityTrustResourceUrl(value: string) { return new LocalSanitizedValue(value); } +} + +function stripStyleWsCharacters(value: string): string { + // color: blue; => color:blue + return value.replace(/;/g, '').replace(/:\s+/g, ':'); +} diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index d1ae1f67a5..50d3a0d0f2 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -11,6 +11,8 @@ import {RenderFlags} from '@angular/core/src/render3'; import {defineComponent, defineDirective} from '../../src/render3/index'; import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, elementStyleNamed, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, loadDirective, projection, projectionDef, text, textBinding} from '../../src/render3/instructions'; import {LViewFlags} from '../../src/render3/interfaces/view'; +import {sanitizeUrl} from '../../src/sanitization/sanitization'; +import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; import {ComponentFixture, containerEl, renderToHtml} from './render_util'; @@ -847,4 +849,65 @@ describe('render3 integration test', () => { }); + describe('sanitization', () => { + it('should sanitize data using the provided sanitization interface', () => { + class SanitizationComp { + static ngComponentDef = defineComponent({ + type: SanitizationComp, + selectors: [['sanitize-this']], + factory: () => new SanitizationComp(), + template: (rf: RenderFlags, ctx: SanitizationComp) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'a'); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'href', bind(ctx.href), sanitizeUrl); + } + } + }); + + private href = ''; + + updateLink(href: any) { this.href = href; } + } + + const sanitizer = new LocalSanitizer((value) => { return 'http://bar'; }); + + const fixture = new ComponentFixture(SanitizationComp, {sanitizer}); + fixture.component.updateLink('http://foo'); + fixture.update(); + + const element = fixture.hostElement.querySelector('a') !; + expect(element.getAttribute('href')).toEqual('http://bar'); + + fixture.component.updateLink(sanitizer.bypassSecurityTrustUrl('http://foo')); + fixture.update(); + + expect(element.getAttribute('href')).toEqual('http://foo'); + }); + }); }); + +class LocalSanitizedValue { + constructor(public value: any) {} + toString() { return this.value; } +} + +class LocalSanitizer implements Sanitizer { + constructor(private _interceptor: (value: string|null|any) => string) {} + + sanitize(context: SecurityContext, value: LocalSanitizedValue|string|null): string|null { + if (value instanceof LocalSanitizedValue) { + return value.toString(); + } + return this._interceptor(value); + } + + bypassSecurityTrustHtml(value: string) {} + bypassSecurityTrustStyle(value: string) {} + bypassSecurityTrustScript(value: string) {} + bypassSecurityTrustResourceUrl(value: string) {} + + bypassSecurityTrustUrl(value: string) { return new LocalSanitizedValue(value); } +} diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 9e523f86bd..655ff053d5 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -16,6 +16,7 @@ import {NG_HOST_SYMBOL, renderTemplate} from '../../src/render3/instructions'; import {DirectiveDefList, DirectiveDefListOrFactory, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeDefListOrFactory, PipeTypesOrFactory} from '../../src/render3/interfaces/definition'; import {LElementNode} from '../../src/render3/interfaces/node'; import {RElement, RText, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer'; +import {Sanitizer} from '../../src/sanitization/security'; import {Type} from '../../src/type'; import {getRendererFactory2} from './imported_renderer2'; @@ -51,6 +52,8 @@ export class TemplateFixture extends BaseFixture { hostNode: LElementNode; private _directiveDefs: DirectiveDefList|null; private _pipeDefs: PipeDefList|null; + private _sanitizer: Sanitizer|null; + /** * * @param createBlock Instructions which go into the creation block: @@ -60,10 +63,12 @@ export class TemplateFixture extends BaseFixture { */ constructor( private createBlock: () => void, private updateBlock: () => void = noop, - directives?: DirectiveTypesOrFactory|null, pipes?: PipeTypesOrFactory|null) { + directives?: DirectiveTypesOrFactory|null, pipes?: PipeTypesOrFactory|null, + sanitizer?: Sanitizer) { super(); this._directiveDefs = toDefs(directives, extractDirectiveDef); this._pipeDefs = toDefs(pipes, extractPipeDef); + this._sanitizer = sanitizer || null; this.hostNode = renderTemplate(this.hostElement, (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { this.createBlock(); @@ -71,7 +76,7 @@ export class TemplateFixture extends BaseFixture { if (rf & RenderFlags.Update) { this.updateBlock(); } - }, null !, domRendererFactory3, null, this._directiveDefs, this._pipeDefs); + }, null !, domRendererFactory3, null, this._directiveDefs, this._pipeDefs, sanitizer); } /** @@ -82,7 +87,7 @@ export class TemplateFixture extends BaseFixture { update(updateBlock?: () => void): void { renderTemplate( this.hostNode.native, updateBlock || this.updateBlock, null !, domRendererFactory3, - this.hostNode, this._directiveDefs, this._pipeDefs); + this.hostNode, this._directiveDefs, this._pipeDefs, this._sanitizer); } } @@ -94,7 +99,9 @@ export class ComponentFixture extends BaseFixture { component: T; requestAnimationFrame: {(fn: () => void): void; flush(): void; queue: (() => void)[];}; - constructor(private componentType: ComponentType, opts: {injector?: Injector} = {}) { + constructor( + private componentType: ComponentType, + opts: {injector?: Injector, sanitizer?: Sanitizer} = {}) { super(); this.requestAnimationFrame = function(fn: () => void) { requestAnimationFrame.queue.push(fn); @@ -106,9 +113,12 @@ export class ComponentFixture extends BaseFixture { } }; - this.component = _renderComponent( - componentType, - {host: this.hostElement, scheduler: this.requestAnimationFrame, injector: opts.injector}); + this.component = _renderComponent(componentType, { + host: this.hostElement, + scheduler: this.requestAnimationFrame, + injector: opts.injector, + sanitizer: opts.sanitizer + }); } update(): void { @@ -195,6 +205,7 @@ export function renderComponent(type: ComponentType, opts?: CreateComponen rendererFactory: opts && opts.rendererFactory || testRendererFactory, host: containerEl, scheduler: requestAnimationFrame, + sanitizer: opts ? opts.sanitizer : undefined, hostFeatures: opts && opts.hostFeatures }); }