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

@ -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 =

View File

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

View File

@ -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) */

View File

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