feat(ivy): support injectable sanitization service (#23809)
PR Close #23809
This commit is contained in:
parent
d2a86872a9
commit
816bc8af17
|
@ -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<T>(
|
|||
opts: CreateComponentOptions = {}): T {
|
||||
ngDevMode && assertComponentType(componentType);
|
||||
const rendererFactory = opts.rendererFactory || domRendererFactory3;
|
||||
const sanitizer = opts.sanitizer || null;
|
||||
const componentDef = (componentType as ComponentType<T>).ngComponentDef as ComponentDef<T>;
|
||||
if (componentDef.type != componentType) componentDef.type = componentType;
|
||||
let component: T;
|
||||
|
@ -144,7 +149,7 @@ export function renderComponent<T>(
|
|||
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 =
|
||||
|
|
|
@ -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<T>(
|
||||
viewId: number, renderer: Renderer3, tView: TView, template: ComponentTemplate<T>| 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<T>(
|
|||
lifecycleStage: LifecycleStage.Init,
|
||||
queries: null,
|
||||
injector: currentView && currentView.injector,
|
||||
sanitizer: sanitizer || null
|
||||
};
|
||||
|
||||
return newView;
|
||||
|
@ -450,8 +456,8 @@ function resetApplicationState() {
|
|||
export function renderTemplate<T>(
|
||||
hostNode: RElement, template: ComponentTemplate<T>, 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<T>(
|
|||
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<T>(
|
|||
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<any>): LElementNode {
|
||||
tag: string, rNode: RElement | null, def: ComponentDef<any>,
|
||||
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<T>(
|
||||
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<T>(index: number, value: T | NO_CHANGE): void {
|
|||
export function elementStyleNamed<T>(
|
||||
index: number, styleName: string, value: T | NO_CHANGE, suffix?: string): void;
|
||||
export function elementStyleNamed<T>(
|
||||
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<T>(
|
||||
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<T>(index: number, instance: T, def: ComponentDef<T>):
|
|||
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);
|
||||
}
|
||||
|
|
|
@ -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) */
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -101,6 +101,9 @@
|
|||
{
|
||||
"name": "firstTemplatePass"
|
||||
},
|
||||
{
|
||||
"name": "getCurrentSanitizer"
|
||||
},
|
||||
{
|
||||
"name": "getDirectiveInstance"
|
||||
},
|
||||
|
|
|
@ -383,6 +383,9 @@
|
|||
{
|
||||
"name": "generatePropertyAliases"
|
||||
},
|
||||
{
|
||||
"name": "getCurrentSanitizer"
|
||||
},
|
||||
{
|
||||
"name": "getDirectiveInstance"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue