feat(ivy): support injectable sanitization service (#23809)

PR Close #23809
This commit is contained in:
Matias Niemelä
2018-05-09 15:30:16 -07:00
parent d2a86872a9
commit 816bc8af17
9 changed files with 363 additions and 23 deletions

View File

@ -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(`<a href="${outputValue}"></a>`);
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(`<a href="${outputValue}"></a>`);
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(`<a href="${outputValue}"></a>`);
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(`<div style="${outputValue}"></div>`);
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(`<div style="${outputValue}"></div>`);
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(`<div style="${outputValue}"></div>`);
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(`<script src="${outputValue}"></script>`);
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(`<script src="${outputValue}"></script>`);
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(`<script src="${outputValue}"></script>`);
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(`<script>${outputValue}</script>`);
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(`<script>${outputValue}</script>`);
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(`<script>${outputValue}</script>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should work for html sanitization', () => {
const s = new LocalMockSanitizer(value => `${value} <!--sanitized-->`);
const t = new TemplateFixture(createDiv, undefined, null, null, s);
const inputValue = '<header></header>';
const outputValue = '<header></header> <!--sanitized-->';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml));
expect(t.html).toEqual(`<div>${outputValue}</div>`);
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('<div onclick="alert(123)"></div>');
const outputValue = '<div onclick="alert(123)"></div>';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml));
expect(t.html).toEqual(`<div>${outputValue}</div>`);
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('<div onclick="alert(123)"></div>');
const outputValue = '<div onclick="alert(123)"></div>-ivy';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml));
expect(t.html).toEqual(`<div>${outputValue}</div>`);
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, ':');
}

View File

@ -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); }
}

View File

@ -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<T> extends BaseFixture {
component: T;
requestAnimationFrame: {(fn: () => void): void; flush(): void; queue: (() => void)[];};
constructor(private componentType: ComponentType<T>, opts: {injector?: Injector} = {}) {
constructor(
private componentType: ComponentType<T>,
opts: {injector?: Injector, sanitizer?: Sanitizer} = {}) {
super();
this.requestAnimationFrame = function(fn: () => void) {
requestAnimationFrame.queue.push(fn);
@ -106,9 +113,12 @@ export class ComponentFixture<T> 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<T>(type: ComponentType<T>, opts?: CreateComponen
rendererFactory: opts && opts.rendererFactory || testRendererFactory,
host: containerEl,
scheduler: requestAnimationFrame,
sanitizer: opts ? opts.sanitizer : undefined,
hostFeatures: opts && opts.hostFeatures
});
}