feat(ivy): support injectable sanitization service (#23809)
PR Close #23809
This commit is contained in:
@ -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, ':');
|
||||
}
|
||||
|
@ -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); }
|
||||
}
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user