diff --git a/modules/benchmarks/src/largetable/render3/table.ts b/modules/benchmarks/src/largetable/render3/table.ts index 852538c4b2..9893af24b7 100644 --- a/modules/benchmarks/src/largetable/render3/table.ts +++ b/modules/benchmarks/src/largetable/render3/table.ts @@ -48,12 +48,12 @@ export class LargeTableComponent { { if (rf2 & RenderFlags.Create) { E(0, 'td'); - s(c0); + s(null, c0); { T(1); } e(); } if (rf2 & RenderFlags.Update) { - sp(0, 0, cell.row % 2 ? '' : 'grey'); + sp(0, 0, null, cell.row % 2 ? '' : 'grey'); t(1, b(cell.value)); } } diff --git a/modules/benchmarks/src/tree/render3/tree.ts b/modules/benchmarks/src/tree/render3/tree.ts index 3fc03b2f81..70b9d43ea2 100644 --- a/modules/benchmarks/src/tree/render3/tree.ts +++ b/modules/benchmarks/src/tree/render3/tree.ts @@ -41,7 +41,7 @@ export class TreeComponent { template: function(rf: RenderFlags, ctx: TreeComponent) { if (rf & RenderFlags.Create) { E(0, 'span'); - s(c0); + s(null, c0); { T(1); } e(); C(2); @@ -114,7 +114,7 @@ export function TreeTpl(rf: RenderFlags, ctx: TreeNode) { E(0, 'tree'); { E(1, 'span'); - s(c1); + s(null, c1); { T(2); } e(); C(3); diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 6fd0a1c9ed..3d549e7665 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -53,7 +53,7 @@ describe('compiler compliance', () => { template: function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵE(0, "div", $c1$); - $r3$.ɵs(null, $c2$); + $r3$.ɵs($c2$); $r3$.ɵNS(); $r3$.ɵE(1, "svg"); $r3$.ɵEe(2, "circle", $c3$); @@ -103,7 +103,7 @@ describe('compiler compliance', () => { template: function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵE(0, "div", $c1$); - $r3$.ɵs(null, $c2$); + $r3$.ɵs($c2$); $r3$.ɵNM(); $r3$.ɵE(1, "math"); $r3$.ɵEe(2, "infinity"); @@ -153,7 +153,7 @@ describe('compiler compliance', () => { template: function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵE(0, "div", $c1$); - $r3$.ɵs(null, $c2$); + $r3$.ɵs($c2$); $r3$.ɵT(1, "Hello "); $r3$.ɵE(2, "b"); $r3$.ɵT(3, "World"); @@ -329,8 +329,8 @@ describe('compiler compliance', () => { const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }'; const template = ` - const _c0 = ["background-color"]; - const _c1 = ["error"]; + const _c0 = ["error"]; + const _c1 = ["background-color"]; … MyComponent.ngComponentDef = i0.ɵdefineComponent({type:MyComponent,selectors:[["my-component"]], factory:function MyComponent_Factory(){ diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index 24ddf9447d..00aa063cae 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -43,11 +43,11 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵE(0, "div"); - $r3$.ɵs(); + $r3$.ɵs(null, null, $r3$.ɵzss); $r3$.ɵe(); } if (rf & 2) { - $r3$.ɵsm(0, $ctx$.myStyleExp); + $r3$.ɵsm(0, null, $ctx$.myStyleExp); $r3$.ɵsa(0); } } @@ -96,15 +96,15 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵE(0, "div"); - $r3$.ɵs(_c0); + $r3$.ɵs(null, _c0, $r3$.ɵzss); $r3$.ɵe(); } if (rf & 2) { - $r3$.ɵsm(0, $ctx$.myStyleExp); + $r3$.ɵsm(0, null, $ctx$.myStyleExp); $r3$.ɵsp(0, 1, $ctx$.myWidth); $r3$.ɵsp(0, 2, $ctx$.myHeight); $r3$.ɵsa(0); - $r3$.ɵa(0, "style", $r3$.ɵb("border-width: 10px")); + $r3$.ɵa(0, "style", $r3$.ɵb("border-width: 10px"), $r3$.ɵzs); } } }); @@ -113,6 +113,59 @@ describe('compiler compliance: styling', () => { const result = compile(files, angularFiles); expectEmit(result.source, template, 'Incorrect template'); }); + + it('should assign a sanitizer instance to the element style allocation instruction if any url-based properties are detected', + () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \`
\` + }) + export class MyComponent { + myImage = 'url(foo.jpg)'; + } + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const template = ` + const _c0 = ["background-image"]; + export class MyComponent { + constructor() { + this.myImage = 'url(foo.jpg)'; + } + } + + MyComponent.ngComponentDef = i0.ɵdefineComponent({ + type: MyComponent, + selectors: [["my-component"]], + factory: function MyComponent_Factory() { + return new MyComponent(); + }, + template: function MyComponent_Template(rf, ctx) { + if (rf & 1) { + i0.ɵE(0, "div"); + i0.ɵs(null, _c0, i0.ɵzss); + i0.ɵe(); + } + if (rf & 2) { + i0.ɵsp(0, 0, ctx.myImage); + i0.ɵsa(0); + } + } + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); }); describe('[class]', () => { @@ -144,7 +197,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵe(); } if (rf & 2) { - $r3$.ɵsm(0,null,$ctx$.myClassExp); + $r3$.ɵsm(0,$ctx$.myClassExp); $r3$.ɵsa(0); } } @@ -193,11 +246,11 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, $ctx$) { if (rf & 1) { $r3$.ɵE(0, "div"); - $r3$.ɵs(null, _c0); + $r3$.ɵs(_c0); $r3$.ɵe(); } if (rf & 2) { - $r3$.ɵsm(0, null, $ctx$.myClassExp); + $r3$.ɵsm(0, $ctx$.myClassExp); $r3$.ɵcp(0, 1, $ctx$.yesToApple); $r3$.ɵcp(0, 2, $ctx$.yesToOrange); $r3$.ɵsa(0); @@ -234,8 +287,8 @@ describe('compiler compliance: styling', () => { }; const template = ` - const _c0 = ["width",${InitialStylingFlags.VALUES_MODE},"width","100px"]; - const _c1 = ["foo",${InitialStylingFlags.VALUES_MODE},"foo",true]; + const _c0 = ["foo",${InitialStylingFlags.VALUES_MODE},"foo",true]; + const _c1 = ["width",${InitialStylingFlags.VALUES_MODE},"width","100px"]; … MyComponent.ngComponentDef = i0.ɵdefineComponent({ type: MyComponent, @@ -251,7 +304,7 @@ describe('compiler compliance: styling', () => { } if (rf & 2) { $r3$.ɵa(0, "class", $r3$.ɵb("round")); - $r3$.ɵa(0, "style", $r3$.ɵb("height:100px")); + $r3$.ɵa(0, "style", $r3$.ɵb("height:100px"), $r3$.ɵzs); } } }); diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index ae60d396a1..5e1aaabd46 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -154,4 +154,12 @@ export class Identifiers { // Reserve slots for pure functions static reserveSlots: o.ExternalReference = {name: 'ɵrS', moduleName: CORE}; + + // sanitization-related functions + static sanitizeHtml: o.ExternalReference = {name: 'ɵzh', moduleName: CORE}; + static sanitizeStyle: o.ExternalReference = {name: 'ɵzs', moduleName: CORE}; + static defaultStyleSanitizer: o.ExternalReference = {name: 'ɵzss', moduleName: CORE}; + static sanitizeResourceUrl: o.ExternalReference = {name: 'ɵzr', moduleName: CORE}; + static sanitizeScript: o.ExternalReference = {name: 'ɵzc', moduleName: CORE}; + static sanitizeUrl: o.ExternalReference = {name: 'ɵzu', moduleName: CORE}; } diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index e50f2a5ce9..a2a937d7ea 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -368,10 +368,13 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } }); + let hasMapBasedStyling = false; for (let i = 0; i < styleInputs.length; i++) { const input = styleInputs[i]; const isMapBasedStyleBinding = i === 0 && input.name === 'style'; - if (!isMapBasedStyleBinding && !stylesIndexMap.hasOwnProperty(input.name)) { + if (isMapBasedStyleBinding) { + hasMapBasedStyling = true; + } else if (!stylesIndexMap.hasOwnProperty(input.name)) { stylesIndexMap[input.name] = currStyleIndex++; } } @@ -384,9 +387,16 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } } + // in the event that a [style] binding is used then sanitization will + // always be imported because it is not possible to know ahead of time + // whether style bindings will use or not use any sanitizable properties + // that isStyleSanitizable() will detect + let useDefaultStyleSanitizer = hasMapBasedStyling; + // this will build the instructions so that they fall into the following syntax // => [prop1, prop2, prop3, 0, prop1, value1, prop2, value2] Object.keys(stylesIndexMap).forEach(prop => { + useDefaultStyleSanitizer = useDefaultStyleSanitizer || isStyleSanitizable(prop); initialStyleDeclarations.push(o.literal(prop)); }); @@ -473,18 +483,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver if (hasStylingInstructions) { const paramsList: (o.Expression)[] = []; - if (initialStyleDeclarations.length) { - // the template compiler handles initial style (e.g. style="foo") values - // in a special command called `elementStyle` so that the initial styles - // can be processed during runtime. These initial styles values are bound to - // a constant because the inital style values do not change (since they're static). - paramsList.push( - this.constantPool.getConstLiteral(o.literalArr(initialStyleDeclarations), true)); - } else if (initialClassDeclarations.length) { - // no point in having an extra `null` value unless there are follow-up params - paramsList.push(o.NULL_EXPR); - } - if (initialClassDeclarations.length) { // the template compiler handles initial class styling (e.g. class="foo") values // in a special command called `elementClass` so that the initial class @@ -492,6 +490,26 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // a constant because the inital class values do not change (since they're static). paramsList.push( this.constantPool.getConstLiteral(o.literalArr(initialClassDeclarations), true)); + } else if (initialStyleDeclarations.length || useDefaultStyleSanitizer) { + // no point in having an extra `null` value unless there are follow-up params + paramsList.push(o.NULL_EXPR); + } + + if (initialStyleDeclarations.length) { + // the template compiler handles initial style (e.g. style="foo") values + // in a special command called `elementStyle` so that the initial styles + // can be processed during runtime. These initial styles values are bound to + // a constant because the inital style values do not change (since they're static). + paramsList.push( + this.constantPool.getConstLiteral(o.literalArr(initialStyleDeclarations), true)); + } else if (useDefaultStyleSanitizer) { + // no point in having an extra `null` value unless there are follow-up params + paramsList.push(o.NULL_EXPR); + } + + + if (useDefaultStyleSanitizer) { + paramsList.push(o.importExpr(R3.defaultStyleSanitizer)); } this._creationCode.push(o.importExpr(R3.elementStyling).callFn(paramsList).toStmt()); @@ -532,13 +550,13 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const stylingInput = mapBasedStyleInput || mapBasedClassInput; if (stylingInput) { const params: o.Expression[] = []; - if (mapBasedStyleInput) { - params.push(this.convertPropertyBinding(implicit, mapBasedStyleInput.value, true)); - } else if (mapBasedClassInput) { - params.push(o.NULL_EXPR); - } if (mapBasedClassInput) { params.push(this.convertPropertyBinding(implicit, mapBasedClassInput.value, true)); + } else if (mapBasedStyleInput) { + params.push(o.NULL_EXPR); + } + if (mapBasedStyleInput) { + params.push(this.convertPropertyBinding(implicit, mapBasedStyleInput.value, true)); } this.instruction( this._bindingCode, stylingInput.sourceSpan, R3.elementStylingMap, indexLiteral, @@ -551,11 +569,17 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver for (i; i < styleInputs.length; i++) { const input = styleInputs[i]; const convertedBinding = this.convertPropertyBinding(implicit, input.value, true); + const params = [convertedBinding]; + const sanitizationRef = resolveSanitizationFn(input, input.securityContext); + if (sanitizationRef) { + params.push(sanitizationRef); + } + const key = input.name; const styleIndex: number = stylesIndexMap[key] !; this.instruction( this._bindingCode, input.sourceSpan, R3.elementStyleProp, indexLiteral, - o.literal(styleIndex), convertedBinding); + o.literal(styleIndex), ...params); } lastInputCommand = styleInputs[styleInputs.length - 1]; @@ -566,11 +590,17 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver for (i; i < classInputs.length; i++) { const input = classInputs[i]; const convertedBinding = this.convertPropertyBinding(implicit, input.value, true); + const params = [convertedBinding]; + const sanitizationRef = resolveSanitizationFn(input, input.securityContext); + if (sanitizationRef) { + params.push(sanitizationRef); + } + const key = input.name; const classIndex: number = classesIndexMap[key] !; this.instruction( this._bindingCode, input.sourceSpan, R3.elementClassProp, indexLiteral, - o.literal(classIndex), convertedBinding); + o.literal(classIndex), ...params); } lastInputCommand = classInputs[classInputs.length - 1]; @@ -588,12 +618,19 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } const convertedBinding = this.convertPropertyBinding(implicit, input.value); + const instruction = mapBindingToInstruction(input.type); if (instruction) { + const params = [convertedBinding]; + const sanitizationRef = resolveSanitizationFn(input, input.securityContext); + if (sanitizationRef) { + params.push(sanitizationRef); + } + // TODO(chuckj): runtime: security context? this.instruction( this._bindingCode, input.sourceSpan, instruction, o.literal(elementIndex), - o.literal(input.name), convertedBinding); + o.literal(input.name), ...params); } else { this._unsupported(`binding type ${input.type}`); } @@ -1061,3 +1098,36 @@ export function makeBindingParser(): BindingParser { function isClassBinding(input: t.BoundAttribute): boolean { return input.name == 'className' || input.name == 'class'; } + +function resolveSanitizationFn(input: t.BoundAttribute, context: core.SecurityContext) { + switch (context) { + case core.SecurityContext.HTML: + return o.importExpr(R3.sanitizeHtml); + case core.SecurityContext.SCRIPT: + return o.importExpr(R3.sanitizeScript); + case core.SecurityContext.STYLE: + // the compiler does not fill in an instruction for [style.prop?] binding + // values because the style algorithm knows internally what props are subject + // to sanitization (only [attr.style] values are explicitly sanitized) + return input.type === BindingType.Attribute ? o.importExpr(R3.sanitizeStyle) : null; + case core.SecurityContext.URL: + return o.importExpr(R3.sanitizeUrl); + case core.SecurityContext.RESOURCE_URL: + return o.importExpr(R3.sanitizeResourceUrl); + default: + return null; + } +} + +function isStyleSanitizable(prop: string): boolean { + switch (prop) { + case 'background-image': + case 'background': + case 'border-image': + case 'filter': + case 'list-style': + case 'list-style-image': + return true; + } + return false; +} diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index d51c1d5468..c88234bda0 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -102,14 +102,16 @@ export { } from './render3/index'; export {NgModuleDef as ɵNgModuleDef} from './metadata/ng_module'; export { - bypassSanitizationTrustHtml as ɵbypassSanitizationTrustHtml, - bypassSanitizationTrustStyle as ɵbypassSanitizationTrustStyle, - bypassSanitizationTrustScript as ɵbypassSanitizationTrustScript, - bypassSanitizationTrustUrl as ɵbypassSanitizationTrustUrl, - bypassSanitizationTrustResourceUrl as ɵbypassSanitizationTrustResourceUrl, sanitizeHtml as ɵsanitizeHtml, sanitizeStyle as ɵsanitizeStyle, sanitizeUrl as ɵsanitizeUrl, sanitizeResourceUrl as ɵsanitizeResourceUrl, } from './sanitization/sanitization'; +export { + bypassSanitizationTrustHtml as ɵbypassSanitizationTrustHtml, + bypassSanitizationTrustStyle as ɵbypassSanitizationTrustStyle, + bypassSanitizationTrustScript as ɵbypassSanitizationTrustScript, + bypassSanitizationTrustUrl as ɵbypassSanitizationTrustUrl, + bypassSanitizationTrustResourceUrl as ɵbypassSanitizationTrustResourceUrl, +} from './sanitization/bypass'; // clang-format on diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 187ada2015..eba6b90974 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -10,6 +10,7 @@ import './ng_dev_mode'; import {QueryList} from '../linker'; import {Sanitizer} from '../sanitization/security'; +import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotEqual} from './assert'; import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; @@ -25,7 +26,7 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, Curre import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; -import {StylingContext, StylingIndex, allocStylingContext, createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling'; +import {StylingContext, allocStylingContext, createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling'; import {assertDataInRangeInternal, isDifferent, loadElementInternal, loadInternal, stringify} from './util'; import {ViewRef} from './view_ref'; @@ -1291,25 +1292,29 @@ export function elementClassProp( * (Note that this is not the element index, but rather an index value allocated * specifically for element styling--the index must be the next index after the element * index.) - * @param styleDeclarations A key/value array of CSS styles that will be registered on the element. - * Each individual style will be used on the element as long as it is not overridden - * by any styles placed on the element by multiple (`[style]`) or singular (`[style.prop]`) - * bindings. If a style binding changes its value to null then the initial styling - * values that are passed in here will be applied to the element (if matched). * @param classDeclarations A key/value array of CSS classes that will be registered on the element. * Each individual style will be used on the element as long as it is not overridden * by any classes placed on the element by multiple (`[class]`) or singular (`[class.named]`) * bindings. If a class binding changes its value to a falsy value then the matching initial * class value that are passed in here will be applied to the element (if matched). + * @param styleDeclarations A key/value array of CSS styles that will be registered on the element. + * Each individual style will be used on the element as long as it is not overridden + * by any styles placed on the element by multiple (`[style]`) or singular (`[style.prop]`) + * bindings. If a style binding changes its value to null then the initial styling + * values that are passed in here will be applied to the element (if matched). + * @param styleSanitizer An optional sanitizer function that will be used (if provided) + * to sanitize the any CSS property values that are applied to the element (during rendering). */ export function elementStyling( + classDeclarations?: (string | boolean | InitialStylingFlags)[] | null, styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, - classDeclarations?: (string | boolean | InitialStylingFlags)[] | null): void { + styleSanitizer?: StyleSanitizeFn | null): void { const lElement = currentElementNode !; const tNode = lElement.tNode; if (!tNode.stylingTemplate) { // initialize the styling template. - tNode.stylingTemplate = createStylingContextTemplate(styleDeclarations, classDeclarations); + tNode.stylingTemplate = + createStylingContextTemplate(classDeclarations, styleDeclarations, styleSanitizer); } if (styleDeclarations && styleDeclarations.length || classDeclarations && classDeclarations.length) { @@ -1377,22 +1382,23 @@ export function elementStylingApply(index: number): void { * renaming as part of minification. * @param value New value to write (null to remove). * @param suffix Optional suffix. Used with scalar values to add unit such as `px`. - * @param sanitizer An optional function used to transform the value typically used for - * sanitization. + * Note that when a suffix is provided then the underlying sanitizer will + * be ignored. */ export function elementStyleProp( - index: number, styleIndex: number, value: T | null, suffix?: string): void; -export function elementStyleProp( - index: number, styleIndex: number, value: T | null, sanitizer?: SanitizerFn): void; -export function elementStyleProp( - index: number, styleIndex: number, value: T | null, - suffixOrSanitizer?: string | SanitizerFn): void { + index: number, styleIndex: number, value: T | null, suffix?: string): void { let valueToAdd: string|null = null; if (value) { - valueToAdd = - typeof suffixOrSanitizer == 'function' ? suffixOrSanitizer(value) : stringify(value); - if (typeof suffixOrSanitizer == 'string') { - valueToAdd = valueToAdd + suffixOrSanitizer; + if (suffix) { + // when a suffix is applied then it will bypass + // sanitization entirely (b/c a new string is created) + valueToAdd = stringify(value) + suffix; + } else { + // sanitization happens by dealing with a String value + // this means that the string value will be passed through + // into the style rendering later (which is where the value + // will be sanitized before it is applied) + valueToAdd = value as any as string; } } updateElementStyleProp(getStylingContext(index), styleIndex, valueToAdd); @@ -1412,17 +1418,17 @@ export function elementStyleProp( * (Note that this is not the element index, but rather an index value allocated * specifically for element styling--the index must be the next index after the element * index.) - * @param styles A key/value style map of the styles that will be applied to the given element. - * Any missing styles (that have already been applied to the element beforehand) will be - * removed (unset) from the element's styling. * @param classes A key/value style map of CSS classes that will be added to the given element. * Any missing classes (that have already been applied to the element beforehand) will be * removed (unset) from the element's list of CSS classes. + * @param styles A key/value style map of the styles that will be applied to the given element. + * Any missing styles (that have already been applied to the element beforehand) will be + * removed (unset) from the element's styling. */ export function elementStylingMap( - index: number, styles: {[styleName: string]: any} | null, - classes?: {[key: string]: any} | string | null): void { - updateStylingMap(getStylingContext(index), styles, classes); + index: number, classes: {[key: string]: any} | string | null, + styles?: {[styleName: string]: any} | null): void { + updateStylingMap(getStylingContext(index), classes, styles); } ////////////////////////// diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 65e122c5f5..9a2137d1d8 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -9,6 +9,7 @@ import {defineInjectable, defineInjector,} from '../../di/defs'; import {inject} from '../../di/injector'; import * as r3 from '../index'; +import * as sanitization from '../../sanitization/sanitization'; /** @@ -88,4 +89,11 @@ export const angularCoreEnv: {[name: string]: Function} = { 'ɵt': r3.t, 'ɵV': r3.V, 'ɵv': r3.v, + + 'ɵzh': sanitization.sanitizeHtml, + 'ɵzs': sanitization.sanitizeStyle, + 'ɵzss': sanitization.defaultStyleSanitizer, + 'ɵzr': sanitization.sanitizeResourceUrl, + 'ɵzc': sanitization.sanitizeScript, + 'ɵzu': sanitization.sanitizeUrl }; diff --git a/packages/core/src/render3/styling.ts b/packages/core/src/render3/styling.ts index d4e2d049da..aa46782341 100644 --- a/packages/core/src/render3/styling.ts +++ b/packages/core/src/render3/styling.ts @@ -6,10 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ +import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {InitialStylingFlags} from './interfaces/definition'; import {LElementNode} from './interfaces/node'; import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; + /** * The styling context acts as a styling manifest (shaped as an array) for determining which * styling properties have been assigned via the provided `updateStylingMap`, `updateStyleProp` @@ -51,42 +53,44 @@ import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces * * ``` * context = [ + * element, + * styleSanitizer | null, * [null, '100px', '200px', true], // property names are not needed since they have already been * written to DOM. * + * configMasterVal, * 1, // this instructs how many `style` values there are so that class index values can be * offsetted - * - * configMasterVal, - * - * // 3 - * 'width', - * pointers(1, 12); // Point to static `width`: `100px` and multi `width`. - * null, + * 'last class string applied', * * // 6 - * 'height', - * pointers(2, 15); // Point to static `height`: `200px` and multi `height`. + * 'width', + * pointers(1, 15); // Point to static `width`: `100px` and multi `width`. * null, * * // 9 - * 'foo', - * pointers(1, 18); // Point to static `foo`: `true` and multi `foo`. + * 'height', + * pointers(2, 18); // Point to static `height`: `200px` and multi `height`. * null, * * // 12 - * 'width', - * pointers(1, 3); // Point to static `width`: `100px` and single `width`. + * 'foo', + * pointers(1, 21); // Point to static `foo`: `true` and multi `foo`. * null, * * // 15 - * 'height', - * pointers(2, 6); // Point to static `height`: `200px` and single `height`. + * 'width', + * pointers(1, 6); // Point to static `width`: `100px` and single `width`. * null, * * // 18 + * 'height', + * pointers(2, 9); // Point to static `height`: `200px` and single `height`. + * null, + * + * // 21 * 'foo', - * pointers(3, 9); // Point to static `foo`: `true` and single `foo`. + * pointers(3, 12); // Point to static `foo`: `true` and single `foo`. * null, * ] * @@ -111,36 +115,41 @@ import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces * `updateStylingMap` can include new CSS properties that will be added to the context). */ export interface StylingContext extends - Array { + Array { /** * Location of element that is used as a target for this context. */ [0]: LElementNode|null; + /** + * The style sanitizer that is used within this context + */ + [1]: StyleSanitizeFn|null; + /** * Location of initial data shared by all instances of this style. */ - [1]: InitialStyles; + [2]: InitialStyles; /** * A numeric value representing the configuration status (whether the context is dirty or not) * mixed together (using bit shifting) with a index value which tells the starting index value * of where the multi style entries begin. */ - [2]: number; + [3]: number; /** * A numeric value representing the class index offset value. Whenever a single class is * applied (using `elementClassProp`) it should have an styling index value that doesn't * need to take into account any style values that exist in the context. */ - [3]: number; + [4]: number; /** * The last CLASS STRING VALUE that was interpreted by elementStylingMap. This is cached * So that the algorithm can exit early incase the string has not changed. */ - [4]: string|null; + [5]: string|null; } /** @@ -159,31 +168,35 @@ export interface InitialStyles extends Array { [0]: null; } */ export const enum StylingFlags { // Implies no configurations - None = 0b00, + None = 0b000, // Whether or not the entry or context itself is dirty - Dirty = 0b01, + Dirty = 0b001, // Whether or not this is a class-based assignment - Class = 0b10, + Class = 0b010, + // Whether or not a sanitizer was applied to this property + Sanitize = 0b100, // The max amount of bits used to represent these configuration values - BitCountSize = 2, - // There are only two bits here - BitMask = 0b11 + BitCountSize = 3, + // There are only three bits here + BitMask = 0b111 } /** Used as numeric pointer values to determine what cells to update in the `StylingContext` */ export const enum StylingIndex { // Position of where the initial styles are stored in the styling context ElementPosition = 0, + // Position of where the style sanitizer is stored within the styling context + StyleSanitizerPosition = 1, // Position of where the initial styles are stored in the styling context - InitialStylesPosition = 1, + InitialStylesPosition = 2, // Index of location where the start of single properties are stored. (`updateStyleProp`) - MasterFlagPosition = 2, + MasterFlagPosition = 3, // Index of location where the class index offset value is located - ClassOffsetPosition = 3, + ClassOffsetPosition = 4, // Position of where the last string-based CSS class value was stored - CachedCssClassString = 4, + CachedCssClassString = 5, // Location of single (prop) value entries are stored within the context - SingleStylesStartPosition = 5, + SingleStylesStartPosition = 6, // Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue FlagsOffset = 0, PropertyOffset = 1, @@ -191,9 +204,9 @@ export const enum StylingIndex { // Size of each multi or single entry (flag + prop + value) Size = 3, // Each flag has a binary digit length of this value - BitCountSize = 15, // (32 - 1) / 2 = ~15 + BitCountSize = 14, // (32 - 3) / 2 = ~14 // The binary digit value as a mask - BitMask = 0b111111111111111 // 15 bits + BitMask = 0b11111111111111 // 14 bits } /** @@ -233,10 +246,11 @@ export function allocStylingContext( * class will be applied to the element as an initial class since it's true */ export function createStylingContextTemplate( + initialClassDeclarations?: (string | boolean | InitialStylingFlags)[] | null, initialStyleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, - initialClassDeclarations?: (string | boolean | InitialStylingFlags)[] | null): StylingContext { + styleSanitizer?: StyleSanitizeFn | null): StylingContext { const initialStylingValues: InitialStyles = [null]; - const context: StylingContext = [null, initialStylingValues, 0, 0, null]; + const context: StylingContext = [null, styleSanitizer || null, initialStylingValues, 0, 0, null]; // we use two maps since a class name might collide with a CSS style prop const stylesLookup: {[key: string]: number} = {}; @@ -314,7 +328,7 @@ export function createStylingContextTemplate( const indexForMulti = i * StylingIndex.Size + multiStart; const indexForSingle = i * StylingIndex.Size + singleStart; - const initialFlag = isClassBased ? StylingFlags.Class : StylingFlags.None; + const initialFlag = prepareInitialFlag(prop, isClassBased, styleSanitizer || null); setFlag(context, indexForSingle, pointers(initialFlag, indexForInitial, indexForMulti)); setProp(context, indexForSingle, prop); @@ -347,12 +361,12 @@ const EMPTY_OBJ: {[key: string]: any} = {}; * * @param context The styling context that will be updated with the * newly provided style values. - * @param styles The key/value map of CSS styles that will be used for the update. * @param classes The key/value map of CSS class names that will be used for the update. + * @param styles The key/value map of CSS styles that will be used for the update. */ export function updateStylingMap( - context: StylingContext, styles: {[key: string]: any} | null, - classes?: {[key: string]: any} | string | null): void { + context: StylingContext, classes: {[key: string]: any} | string | null, + styles?: {[key: string]: any} | null): void { let classNames: string[] = EMPTY_ARR; let applyAllClasses = false; let ignoreAllClassUpdates = false; @@ -407,10 +421,10 @@ export function updateStylingMap( const prop = getProp(context, ctxIndex); if (prop === newProp) { const value = getValue(context, ctxIndex); - if (value !== newValue) { + const flag = getPointers(context, ctxIndex); + if (hasValueChanged(flag, value, newValue)) { setValue(context, ctxIndex, newValue); - const flag = getPointers(context, ctxIndex); const initialValue = getInitialValue(context, flag); // there is no point in setting this to dirty if the previously @@ -437,7 +451,8 @@ export function updateStylingMap( } } else { // we only care to do this if the insertion is in the middle - insertNewMultiProperty(context, ctxIndex, isClassBased, newProp, newValue); + const newFlag = prepareInitialFlag(newProp, isClassBased, getStyleSanitizer(context)); + insertNewMultiProperty(context, ctxIndex, isClassBased, newProp, newFlag, newValue); dirty = true; } } @@ -468,6 +483,7 @@ export function updateStylingMap( // this means that there are left-over properties in the context that // were not detected in the context during the loop above. In that // case we want to add the new entries into the list + const sanitizer = getStyleSanitizer(context); while (propIndex < propLimit) { const isClassBased = propIndex >= classesStartIndex; if (ignoreAllClassUpdates && isClassBased) break; @@ -476,7 +492,7 @@ export function updateStylingMap( const prop = isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex]; const value: string|boolean = isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop]; - const flag = StylingFlags.Dirty | (isClassBased ? StylingFlags.Class : StylingFlags.None); + const flag = prepareInitialFlag(prop, isClassBased, sanitizer) | StylingFlags.Dirty; context.push(flag, prop, value); propIndex++; dirty = true; @@ -508,7 +524,7 @@ export function updateStyleProp( const currFlag = getPointers(context, singleIndex); // didn't change ... nothing to make a note of - if (currValue !== value) { + if (hasValueChanged(currFlag, currValue, value)) { // the value will always get updated (even if the dirty flag is skipped) setValue(context, singleIndex, value); const indexForMulti = getMultiOrSingleIndex(currFlag); @@ -573,6 +589,7 @@ export function renderStyling( if (isContextDirty(context)) { const native = context[StylingIndex.ElementPosition] !.native; const multiStartIndex = getMultiStartIndex(context); + const styleSanitizer = getStyleSanitizer(context); for (let i = StylingIndex.SingleStylesStartPosition; i < context.length; i += StylingIndex.Size) { // there is no point in rendering styles that have not changed on screen @@ -607,7 +624,8 @@ export function renderStyling( if (isClassBased) { setClass(native, prop, valueToApply ? true : false, renderer, classStore); } else { - setStyle(native, prop, valueToApply as string | null, renderer, styleStore); + const sanitizer = (flag & StylingFlags.Sanitize) ? styleSanitizer : null; + setStyle(native, prop, valueToApply as string | null, renderer, sanitizer, styleStore); } setDirty(context, i, false); } @@ -631,7 +649,8 @@ export function renderStyling( */ function setStyle( native: any, prop: string, value: string | null, renderer: Renderer3, - store?: {[key: string]: any}) { + sanitizer: StyleSanitizeFn | null, store?: {[key: string]: any}) { + value = sanitizer && value ? sanitizer(prop, value) : value; if (store) { store[prop] = value; } else if (value) { @@ -697,6 +716,12 @@ function isClassBased(context: StylingContext, index: number): boolean { return ((context[adjustedIndex] as number) & StylingFlags.Class) == StylingFlags.Class; } +function isSanitizable(context: StylingContext, index: number): boolean { + const adjustedIndex = + index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index; + return ((context[adjustedIndex] as number) & StylingFlags.Sanitize) == StylingFlags.Sanitize; +} + function pointers(configFlag: number, staticIndex: number, dynamicIndex: number) { return (configFlag & StylingFlags.BitMask) | (staticIndex << StylingFlags.BitCountSize) | (dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize)); @@ -721,6 +746,10 @@ function getMultiStartIndex(context: StylingContext): number { return getMultiOrSingleIndex(context[StylingIndex.MasterFlagPosition]) as number; } +function getStyleSanitizer(context: StylingContext): StyleSanitizeFn|null { + return context[StylingIndex.StyleSanitizerPosition]; +} + function setProp(context: StylingContext, index: number, prop: string) { context[index + StylingIndex.PropertyOffset] = prop; } @@ -808,7 +837,8 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition: const singleFlag = getPointers(context, singleIndex); const initialIndexForSingle = getInitialIndex(singleFlag); const flagValue = (isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None) | - (isClassBased(context, singleIndex) ? StylingFlags.Class : StylingFlags.None); + (isClassBased(context, singleIndex) ? StylingFlags.Class : StylingFlags.None) | + (isSanitizable(context, singleIndex) ? StylingFlags.Sanitize : StylingFlags.None); const updatedFlag = pointers(flagValue, initialIndexForSingle, i); setFlag(context, singleIndex, updatedFlag); } @@ -816,14 +846,14 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition: } function insertNewMultiProperty( - context: StylingContext, index: number, classBased: boolean, name: string, + context: StylingContext, index: number, classBased: boolean, name: string, flag: number, value: string | boolean): void { const doShift = index < context.length; // prop does not exist in the list, add it in context.splice( - index, 0, StylingFlags.Dirty | (classBased ? StylingFlags.Class : StylingFlags.None), name, - value); + index, 0, flag | StylingFlags.Dirty | (classBased ? StylingFlags.Class : StylingFlags.None), + name, value); if (doShift) { // because the value was inserted midway into the array then we @@ -839,3 +869,30 @@ function valueExists(value: string | null | boolean, isClassBased?: boolean) { } return value !== null; } + +function prepareInitialFlag( + name: string, isClassBased: boolean, sanitizer?: StyleSanitizeFn | null) { + if (isClassBased) { + return StylingFlags.Class; + } else if (sanitizer && sanitizer(name)) { + return StylingFlags.Sanitize; + } + return StylingFlags.None; +} + +function hasValueChanged( + flag: number, a: string | boolean | null, b: string | boolean | null): boolean { + const isClassBased = flag & StylingFlags.Class; + const hasValues = a && b; + const usesSanitizer = flag & StylingFlags.Sanitize; + // the toString() comparison ensures that a value is checked + // ... otherwise (during sanitization bypassing) the === comparsion + // would fail since a new String() instance is created + if (!isClassBased && hasValues && usesSanitizer) { + // we know for sure we're dealing with strings at this point + return (a as string).toString() !== (b as string).toString(); + } + + // everything else is safe to check with a normal equality check + return a !== b; +} diff --git a/packages/core/src/sanitization/bypass.ts b/packages/core/src/sanitization/bypass.ts new file mode 100644 index 0000000000..a8b812e225 --- /dev/null +++ b/packages/core/src/sanitization/bypass.ts @@ -0,0 +1,143 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +const BRAND = '__SANITIZER_TRUSTED_BRAND__'; + +export const enum BypassType { + Url = 'Url', + Html = 'Html', + ResourceUrl = 'ResourceUrl', + Script = 'Script', + Style = 'Style', +} + +/** + * A branded trusted string used with sanitization. + * + * See: {@link TrustedHtmlString}, {@link TrustedResourceUrlString}, {@link TrustedScriptString}, + * {@link TrustedStyleString}, {@link TrustedUrlString} + */ +export interface TrustedString extends String { [BRAND]: BypassType; } + +/** + * A branded trusted string used with sanitization of `html` strings. + * + * See: {@link bypassSanitizationTrustHtml} and {@link htmlSanitizer}. + */ +export interface TrustedHtmlString extends TrustedString { [BRAND]: BypassType.Html; } + +/** + * A branded trusted string used with sanitization of `style` strings. + * + * See: {@link bypassSanitizationTrustStyle} and {@link styleSanitizer}. + */ +export interface TrustedStyleString extends TrustedString { [BRAND]: BypassType.Style; } + +/** + * A branded trusted string used with sanitization of `url` strings. + * + * See: {@link bypassSanitizationTrustScript} and {@link scriptSanitizer}. + */ +export interface TrustedScriptString extends TrustedString { [BRAND]: BypassType.Script; } + +/** + * A branded trusted string used with sanitization of `url` strings. + * + * See: {@link bypassSanitizationTrustUrl} and {@link urlSanitizer}. + */ +export interface TrustedUrlString extends TrustedString { [BRAND]: BypassType.Url; } + +/** + * A branded trusted string used with sanitization of `resourceUrl` strings. + * + * See: {@link bypassSanitizationTrustResourceUrl} and {@link resourceUrlSanitizer}. + */ +export interface TrustedResourceUrlString extends TrustedString { [BRAND]: BypassType.ResourceUrl; } + +export function allowSanitizationBypass(value: any, type: BypassType): boolean { + return (value instanceof String && (value as TrustedStyleString)[BRAND] === type) ? true : false; +} + +/** + * Mark `html` string as trusted. + * + * This function wraps the trusted string in `String` and brands it in a way which makes it + * recognizable to {@link htmlSanitizer} to be trusted implicitly. + * + * @param trustedHtml `html` string which needs to be implicitly trusted. + * @returns a `html` `String` which has been branded to be implicitly trusted. + */ +export function bypassSanitizationTrustHtml(trustedHtml: string): TrustedHtmlString { + return bypassSanitizationTrustString(trustedHtml, BypassType.Html); +} +/** + * Mark `style` string as trusted. + * + * This function wraps the trusted string in `String` and brands it in a way which makes it + * recognizable to {@link styleSanitizer} to be trusted implicitly. + * + * @param trustedStyle `style` string which needs to be implicitly trusted. + * @returns a `style` `String` which has been branded to be implicitly trusted. + */ +export function bypassSanitizationTrustStyle(trustedStyle: string): TrustedStyleString { + return bypassSanitizationTrustString(trustedStyle, BypassType.Style); +} +/** + * Mark `script` string as trusted. + * + * This function wraps the trusted string in `String` and brands it in a way which makes it + * recognizable to {@link scriptSanitizer} to be trusted implicitly. + * + * @param trustedScript `script` string which needs to be implicitly trusted. + * @returns a `script` `String` which has been branded to be implicitly trusted. + */ +export function bypassSanitizationTrustScript(trustedScript: string): TrustedScriptString { + return bypassSanitizationTrustString(trustedScript, BypassType.Script); +} +/** + * Mark `url` string as trusted. + * + * This function wraps the trusted string in `String` and brands it in a way which makes it + * recognizable to {@link urlSanitizer} to be trusted implicitly. + * + * @param trustedUrl `url` string which needs to be implicitly trusted. + * @returns a `url` `String` which has been branded to be implicitly trusted. + */ +export function bypassSanitizationTrustUrl(trustedUrl: string): TrustedUrlString { + return bypassSanitizationTrustString(trustedUrl, BypassType.Url); +} +/** + * Mark `url` string as trusted. + * + * This function wraps the trusted string in `String` and brands it in a way which makes it + * recognizable to {@link resourceUrlSanitizer} to be trusted implicitly. + * + * @param trustedResourceUrl `url` string which needs to be implicitly trusted. + * @returns a `url` `String` which has been branded to be implicitly trusted. + */ +export function bypassSanitizationTrustResourceUrl(trustedResourceUrl: string): + TrustedResourceUrlString { + return bypassSanitizationTrustString(trustedResourceUrl, BypassType.ResourceUrl); +} + + +function bypassSanitizationTrustString( + trustedString: string, mode: BypassType.Html): TrustedHtmlString; +function bypassSanitizationTrustString( + trustedString: string, mode: BypassType.Style): TrustedStyleString; +function bypassSanitizationTrustString( + trustedString: string, mode: BypassType.Script): TrustedScriptString; +function bypassSanitizationTrustString( + trustedString: string, mode: BypassType.Url): TrustedUrlString; +function bypassSanitizationTrustString( + trustedString: string, mode: BypassType.ResourceUrl): TrustedResourceUrlString; +function bypassSanitizationTrustString(trustedString: string, mode: BypassType): TrustedString { + const trusted = new String(trustedString) as TrustedString; + trusted[BRAND] = mode; + return trusted; +} diff --git a/packages/core/src/sanitization/sanitization.ts b/packages/core/src/sanitization/sanitization.ts index ecd0203456..c5fac7314a 100644 --- a/packages/core/src/sanitization/sanitization.ts +++ b/packages/core/src/sanitization/sanitization.ts @@ -9,63 +9,13 @@ import {getCurrentSanitizer} from '../render3/instructions'; import {stringify} from '../render3/util'; +import {BypassType, allowSanitizationBypass} from './bypass'; import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer'; import {SecurityContext} from './security'; -import {_sanitizeStyle as _sanitizeStyle} from './style_sanitizer'; +import {StyleSanitizeFn, _sanitizeStyle as _sanitizeStyle} from './style_sanitizer'; import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer'; -const BRAND = '__SANITIZER_TRUSTED_BRAND__'; -/** - * A branded trusted string used with sanitization. - * - * See: {@link TrustedHtmlString}, {@link TrustedResourceUrlString}, {@link TrustedScriptString}, - * {@link TrustedStyleString}, {@link TrustedUrlString} - */ -export interface TrustedString extends String { - '__SANITIZER_TRUSTED_BRAND__': 'Html'|'Style'|'Script'|'Url'|'ResourceUrl'; -} - -/** - * A branded trusted string used with sanitization of `html` strings. - * - * See: {@link bypassSanitizationTrustHtml} and {@link htmlSanitizer}. - */ -export interface TrustedHtmlString extends TrustedString { '__SANITIZER_TRUSTED_BRAND__': 'Html'; } - -/** - * A branded trusted string used with sanitization of `style` strings. - * - * See: {@link bypassSanitizationTrustStyle} and {@link styleSanitizer}. - */ -export interface TrustedStyleString extends TrustedString { - '__SANITIZER_TRUSTED_BRAND__': 'Style'; -} - -/** - * A branded trusted string used with sanitization of `url` strings. - * - * See: {@link bypassSanitizationTrustScript} and {@link scriptSanitizer}. - */ -export interface TrustedScriptString extends TrustedString { - '__SANITIZER_TRUSTED_BRAND__': 'Script'; -} - -/** - * A branded trusted string used with sanitization of `url` strings. - * - * See: {@link bypassSanitizationTrustUrl} and {@link urlSanitizer}. - */ -export interface TrustedUrlString extends TrustedString { '__SANITIZER_TRUSTED_BRAND__': 'Url'; } - -/** - * A branded trusted string used with sanitization of `resourceUrl` strings. - * - * See: {@link bypassSanitizationTrustResourceUrl} and {@link resourceUrlSanitizer}. - */ -export interface TrustedResourceUrlString extends TrustedString { - '__SANITIZER_TRUSTED_BRAND__': 'ResourceUrl'; -} /** * An `html` sanitizer which converts untrusted `html` **string** into trusted string by removing @@ -85,7 +35,7 @@ export function sanitizeHtml(unsafeHtml: any): string { if (s) { return s.sanitize(SecurityContext.HTML, unsafeHtml) || ''; } - if (unsafeHtml instanceof String && (unsafeHtml as TrustedHtmlString)[BRAND] === 'Html') { + if (allowSanitizationBypass(unsafeHtml, BypassType.Html)) { return unsafeHtml.toString(); } return _sanitizeHtml(document, stringify(unsafeHtml)); @@ -109,7 +59,7 @@ export function sanitizeStyle(unsafeStyle: any): string { if (s) { return s.sanitize(SecurityContext.STYLE, unsafeStyle) || ''; } - if (unsafeStyle instanceof String && (unsafeStyle as TrustedStyleString)[BRAND] === 'Style') { + if (allowSanitizationBypass(unsafeStyle, BypassType.Style)) { return unsafeStyle.toString(); } return _sanitizeStyle(stringify(unsafeStyle)); @@ -134,7 +84,7 @@ export function sanitizeUrl(unsafeUrl: any): string { if (s) { return s.sanitize(SecurityContext.URL, unsafeUrl) || ''; } - if (unsafeUrl instanceof String && (unsafeUrl as TrustedUrlString)[BRAND] === 'Url') { + if (allowSanitizationBypass(unsafeUrl, BypassType.Url)) { return unsafeUrl.toString(); } return _sanitizeUrl(stringify(unsafeUrl)); @@ -154,8 +104,7 @@ export function sanitizeResourceUrl(unsafeResourceUrl: any): string { if (s) { return s.sanitize(SecurityContext.RESOURCE_URL, unsafeResourceUrl) || ''; } - if (unsafeResourceUrl instanceof String && - (unsafeResourceUrl as TrustedResourceUrlString)[BRAND] === 'ResourceUrl') { + if (allowSanitizationBypass(unsafeResourceUrl, BypassType.ResourceUrl)) { return unsafeResourceUrl.toString(); } throw new Error('unsafe value used in a resource URL context (see http://g.co/ng/security#xss)'); @@ -175,85 +124,22 @@ export function sanitizeScript(unsafeScript: any): string { if (s) { return s.sanitize(SecurityContext.SCRIPT, unsafeScript) || ''; } - if (unsafeScript instanceof String && (unsafeScript as TrustedScriptString)[BRAND] === 'Script') { + if (allowSanitizationBypass(unsafeScript, BypassType.Script)) { return unsafeScript.toString(); } throw new Error('unsafe value used in a script context'); } /** - * Mark `html` string as trusted. - * - * This function wraps the trusted string in `String` and brands it in a way which makes it - * recognizable to {@link htmlSanitizer} to be trusted implicitly. - * - * @param trustedHtml `html` string which needs to be implicitly trusted. - * @returns a `html` `String` which has been branded to be implicitly trusted. + * The default style sanitizer will handle sanitization for style properties by + * sanitizing any CSS property that can include a `url` value (usually image-based properties) */ -export function bypassSanitizationTrustHtml(trustedHtml: string): TrustedHtmlString { - return bypassSanitizationTrustString(trustedHtml, 'Html'); -} -/** - * Mark `style` string as trusted. - * - * This function wraps the trusted string in `String` and brands it in a way which makes it - * recognizable to {@link styleSanitizer} to be trusted implicitly. - * - * @param trustedStyle `style` string which needs to be implicitly trusted. - * @returns a `style` `String` which has been branded to be implicitly trusted. - */ -export function bypassSanitizationTrustStyle(trustedStyle: string): TrustedStyleString { - return bypassSanitizationTrustString(trustedStyle, 'Style'); -} -/** - * Mark `script` string as trusted. - * - * This function wraps the trusted string in `String` and brands it in a way which makes it - * recognizable to {@link scriptSanitizer} to be trusted implicitly. - * - * @param trustedScript `script` string which needs to be implicitly trusted. - * @returns a `script` `String` which has been branded to be implicitly trusted. - */ -export function bypassSanitizationTrustScript(trustedScript: string): TrustedScriptString { - return bypassSanitizationTrustString(trustedScript, 'Script'); -} -/** - * Mark `url` string as trusted. - * - * This function wraps the trusted string in `String` and brands it in a way which makes it - * recognizable to {@link urlSanitizer} to be trusted implicitly. - * - * @param trustedUrl `url` string which needs to be implicitly trusted. - * @returns a `url` `String` which has been branded to be implicitly trusted. - */ -export function bypassSanitizationTrustUrl(trustedUrl: string): TrustedUrlString { - return bypassSanitizationTrustString(trustedUrl, 'Url'); -} -/** - * Mark `url` string as trusted. - * - * This function wraps the trusted string in `String` and brands it in a way which makes it - * recognizable to {@link resourceUrlSanitizer} to be trusted implicitly. - * - * @param trustedResourceUrl `url` string which needs to be implicitly trusted. - * @returns a `url` `String` which has been branded to be implicitly trusted. - */ -export function bypassSanitizationTrustResourceUrl(trustedResourceUrl: string): - TrustedResourceUrlString { - return bypassSanitizationTrustString(trustedResourceUrl, 'ResourceUrl'); -} +export const defaultStyleSanitizer = (function(prop: string, value?: string): string | boolean { + if (value === undefined) { + return prop === 'background-image' || prop === 'background' || prop === 'border-image' || + prop === 'filter' || prop === 'filter' || prop === 'list-style' || + prop === 'list-style-image'; + } - -function bypassSanitizationTrustString(trustedString: string, mode: 'Html'): TrustedHtmlString; -function bypassSanitizationTrustString(trustedString: string, mode: 'Style'): TrustedStyleString; -function bypassSanitizationTrustString(trustedString: string, mode: 'Script'): TrustedScriptString; -function bypassSanitizationTrustString(trustedString: string, mode: 'Url'): TrustedUrlString; -function bypassSanitizationTrustString( - trustedString: string, mode: 'ResourceUrl'): TrustedResourceUrlString; -function bypassSanitizationTrustString( - trustedString: string, - mode: 'Html' | 'Style' | 'Script' | 'Url' | 'ResourceUrl'): TrustedString { - const trusted = new String(trustedString) as TrustedString; - trusted[BRAND] = mode; - return trusted; -} + return sanitizeStyle(value); +} as StyleSanitizeFn); diff --git a/packages/core/src/sanitization/style_sanitizer.ts b/packages/core/src/sanitization/style_sanitizer.ts index c335781048..88a03e8e98 100644 --- a/packages/core/src/sanitization/style_sanitizer.ts +++ b/packages/core/src/sanitization/style_sanitizer.ts @@ -101,3 +101,19 @@ export function _sanitizeStyle(value: string): string { return 'unsafe'; } + + +/** + * Used to intercept and sanitize style values before they are written to the renderer. + * + * This function is designed to be called in two modes. When a value is not provided + * then the function will return a boolean whether a property will be sanitized later. + * If a value is provided then the sanitized version of that will be returned. + */ +export interface StyleSanitizeFn { + /** This mode is designed to instruct whether the property will be used for sanitization + * at a later point */ + (prop: string): boolean; + /** This mode is designed to sanitize the provided value */ + (prop: string, value: string): string; +} diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 86469d9074..2957b13643 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -539,6 +539,9 @@ { "name": "getRootView" }, + { + "name": "getStyleSanitizer" + }, { "name": "getStylingContext" }, @@ -557,6 +560,9 @@ { "name": "getValue" }, + { + "name": "hasValueChanged" + }, { "name": "hostElement" }, @@ -668,6 +674,9 @@ { "name": "pointers" }, + { + "name": "prepareInitialFlag" + }, { "name": "projectionNodeStack" }, diff --git a/packages/core/test/render3/compiler_canonical/elements_spec.ts b/packages/core/test/render3/compiler_canonical/elements_spec.ts index 1d1c8dd41a..1eb490df7e 100644 --- a/packages/core/test/render3/compiler_canonical/elements_spec.ts +++ b/packages/core/test/render3/compiler_canonical/elements_spec.ts @@ -284,7 +284,7 @@ describe('elements', () => { template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { if (rf & 1) { $r3$.ɵE(0, 'div'); - $r3$.ɵs(null, c1); + $r3$.ɵs(c1); $r3$.ɵe(); } if (rf & 2) { @@ -323,7 +323,7 @@ describe('elements', () => { template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { if (rf & 1) { $r3$.ɵE(0, 'div'); - $r3$.ɵs(c0); + $r3$.ɵs(null, c0); $r3$.ɵe(); } if (rf & 2) { @@ -356,8 +356,8 @@ describe('elements', () => { it('should bind to many and keep order', () => { type $MyComponent$ = MyComponent; - const c0 = ['color', InitialStylingFlags.VALUES_MODE, 'color', 'red']; - const c1 = ['foo']; + const c0 = ['foo']; + const c1 = ['color', InitialStylingFlags.VALUES_MODE, 'color', 'red']; @Component({ selector: 'my-component', @@ -416,7 +416,7 @@ describe('elements', () => { $r3$.ɵe(); } if (rf & 2) { - $r3$.ɵsm(0, ctx.styleExp, ctx.classExp); + $r3$.ɵsm(0, ctx.classExp, ctx.styleExp); $r3$.ɵsa(0); } } diff --git a/packages/core/test/render3/compiler_canonical/sanitize_spec.ts b/packages/core/test/render3/compiler_canonical/sanitize_spec.ts index 43fea1b77b..dd2e4b11cb 100644 --- a/packages/core/test/render3/compiler_canonical/sanitize_spec.ts +++ b/packages/core/test/render3/compiler_canonical/sanitize_spec.ts @@ -51,7 +51,7 @@ describe('compiler sanitization', () => { if (rf & 2) { $r3$.ɵp(0, 'innerHTML', $r3$.ɵb(ctx.innerHTML), $r3$.ɵsanitizeHtml); $r3$.ɵp(0, 'hidden', $r3$.ɵb(ctx.hidden)); - $r3$.ɵsp(0, 0, ctx.style, $r3$.ɵsanitizeStyle); + $r3$.ɵsp(0, 0, ctx.style); $r3$.ɵsa(0); $r3$.ɵp(1, 'src', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl); $r3$.ɵa(1, 'srcset', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl); diff --git a/packages/core/test/render3/exports_spec.ts b/packages/core/test/render3/exports_spec.ts index 8db0c81559..b357572fd5 100644 --- a/packages/core/test/render3/exports_spec.ts +++ b/packages/core/test/render3/exports_spec.ts @@ -213,7 +213,7 @@ describe('exports', () => { function Template(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'div'); - elementStyling(null, [InitialStylingFlags.VALUES_MODE, 'red', true]); + elementStyling([InitialStylingFlags.VALUES_MODE, 'red', true]); elementEnd(); elementStart(1, 'input', ['type', 'checkbox', 'checked', 'true'], ['myInput', '']); elementEnd(); diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index 2e3f74a812..e95b47ba57 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -14,8 +14,10 @@ import {bind, container, element, elementAttribute, elementEnd, elementProperty, import {InitialStylingFlags} from '../../src/render3/interfaces/definition'; import {AttributeMarker, LElementNode, LNode} from '../../src/render3/interfaces/node'; import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer'; -import {TrustedString, bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization'; +import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl} from '../../src/sanitization/bypass'; +import {defaultStyleSanitizer, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization'; import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; +import {StyleSanitizeFn} from '../../src/sanitization/style_sanitizer'; import {NgForOf} from './common_with_def'; import {ComponentFixture, TemplateFixture} from './render_util'; @@ -27,9 +29,10 @@ describe('instructions', () => { elementEnd(); } - function createDiv(initialStyles?: (string | number)[]) { + function createDiv(initialStyles?: (string | number)[], styleSanitizer?: StyleSanitizeFn) { elementStart(0, 'div'); - elementStyling(initialStyles && Array.isArray(initialStyles) ? initialStyles : null); + elementStyling( + [], initialStyles && Array.isArray(initialStyles) ? initialStyles : null, styleSanitizer); elementEnd(); } @@ -190,39 +193,87 @@ describe('instructions', () => { }); describe('elementStyleProp', () => { - it('should use sanitizer function', () => { - const t = new TemplateFixture(() => { return createDiv(['background-image']); }); + it('should automatically sanitize unless a bypass operation is applied', () => { + const t = new TemplateFixture( + () => { return createDiv(['background-image'], defaultStyleSanitizer); }); t.update(() => { - elementStyleProp(0, 0, 'url("http://server")', sanitizeStyle); + elementStyleProp(0, 0, 'url("http://server")'); elementStylingApply(0); }); // nothing is set because sanitizer suppresses it. expect(t.html).toEqual('
'); t.update(() => { - elementStyleProp(0, 0, bypassSanitizationTrustStyle('url("http://server")'), sanitizeStyle); + elementStyleProp(0, 0, bypassSanitizationTrustStyle('url("http://server2")')); elementStylingApply(0); }); expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image')) - .toEqual('url("http://server")'); + .toEqual('url("http://server2")'); + }); + + it('should not re-apply the style value even if it is a newly bypassed again', () => { + const sanitizerInterceptor = new MockSanitizerInterceptor(); + const t = createTemplateFixtureWithSanitizer( + () => createDiv(['background-image'], sanitizerInterceptor.getStyleSanitizer()), + sanitizerInterceptor); + + t.update(() => { + elementStyleProp(0, 0, bypassSanitizationTrustStyle('apple')); + elementStylingApply(0); + }); + + expect(sanitizerInterceptor.lastValue !).toEqual('apple'); + sanitizerInterceptor.lastValue = null; + + t.update(() => { + elementStyleProp(0, 0, bypassSanitizationTrustStyle('apple')); + elementStylingApply(0); + }); + expect(sanitizerInterceptor.lastValue).toEqual(null); }); }); describe('elementStyleMap', () => { function createDivWithStyle() { elementStart(0, 'div'); - elementStyling(['height', InitialStylingFlags.VALUES_MODE, 'height', '10px']); + elementStyling([], ['height', InitialStylingFlags.VALUES_MODE, 'height', '10px']); elementEnd(); } it('should add style', () => { const fixture = new TemplateFixture(createDivWithStyle); fixture.update(() => { - elementStylingMap(0, {'background-color': 'red'}); + elementStylingMap(0, null, {'background-color': 'red'}); elementStylingApply(0); }); expect(fixture.html).toEqual('
'); }); + + it('should sanitize new styles that may contain `url` properties', () => { + const detectedValues: string[] = []; + const sanitizerInterceptor = + new MockSanitizerInterceptor(value => { detectedValues.push(value); }); + const fixture = createTemplateFixtureWithSanitizer( + () => createDiv([], sanitizerInterceptor.getStyleSanitizer()), sanitizerInterceptor); + + fixture.update(() => { + elementStylingMap(0, null, { + 'background-image': 'background-image', + 'background': 'background', + 'border-image': 'border-image', + 'list-style': 'list-style', + 'list-style-image': 'list-style-image', + 'filter': 'filter', + 'width': 'width' + }); + elementStylingApply(0); + }); + + const props = detectedValues.sort(); + expect(props).toEqual([ + 'background', 'background-image', 'border-image', 'filter', 'list-style', 'list-style-image' + ]); + }); }); describe('elementClass', () => { @@ -235,7 +286,7 @@ describe('instructions', () => { it('should add class', () => { const fixture = new TemplateFixture(createDivWithStyling); fixture.update(() => { - elementStylingMap(0, null, 'multiple classes'); + elementStylingMap(0, 'multiple classes'); elementStylingApply(0); }); expect(fixture.html).toEqual('
'); @@ -504,7 +555,23 @@ class LocalMockSanitizer implements Sanitizer { bypassSecurityTrustResourceUrl(value: string) { return new LocalSanitizedValue(value); } } +class MockSanitizerInterceptor { + public lastValue: string|null = null; + constructor(private _interceptorFn?: ((value: any) => any)|null) {} + getStyleSanitizer() { return defaultStyleSanitizer; } + sanitize(context: SecurityContext, value: LocalSanitizedValue|string|null|any): string|null { + if (this._interceptorFn) { + this._interceptorFn(value); + } + return this.lastValue = value; + } +} + function stripStyleWsCharacters(value: string): string { // color: blue; => color:blue return value.replace(/;/g, '').replace(/:\s+/g, ':'); } + +function createTemplateFixtureWithSanitizer(buildFn: () => any, sanitizer: Sanitizer) { + return new TemplateFixture(buildFn, () => {}, null, null, sanitizer); +} diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 1f1182e012..c1e7440471 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -748,7 +748,7 @@ describe('render3 integration test', () => { function Template(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'span'); - elementStyling(['border-color']); + elementStyling(null, ['border-color']); elementEnd(); } if (rf & RenderFlags.Update) { @@ -767,7 +767,7 @@ describe('render3 integration test', () => { function Template(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'span'); - elementStyling(['font-size']); + elementStyling(null, ['font-size']); elementEnd(); } if (rf & RenderFlags.Update) { @@ -788,7 +788,7 @@ describe('render3 integration test', () => { function Template(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'span'); - elementStyling(null, ['active']); + elementStyling(['active']); elementEnd(); } if (rf & RenderFlags.Update) { @@ -814,7 +814,7 @@ describe('render3 integration test', () => { if (rf & RenderFlags.Create) { elementStart(0, 'span'); elementStyling( - null, ['existing', 'active', InitialStylingFlags.VALUES_MODE, 'existing', true]); + ['existing', 'active', InitialStylingFlags.VALUES_MODE, 'existing', true]); elementEnd(); } if (rf & RenderFlags.Update) { diff --git a/packages/core/test/render3/styling_spec.ts b/packages/core/test/render3/styling_spec.ts index c7c558d6f1..46415d86ba 100644 --- a/packages/core/test/render3/styling_spec.ts +++ b/packages/core/test/render3/styling_spec.ts @@ -10,6 +10,8 @@ import {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/def import {LElementNode} from '../../src/render3/interfaces/node'; import {Renderer3} from '../../src/render3/interfaces/renderer'; import {StylingContext, StylingFlags, StylingIndex, allocStylingContext, createStylingContextTemplate, isContextDirty, renderStyling as _renderStyling, setContextDirty, updateClassProp, updateStyleProp, updateStylingMap} from '../../src/render3/styling'; +import {defaultStyleSanitizer} from '../../src/sanitization/sanitization'; +import {StyleSanitizeFn} from '../../src/sanitization/style_sanitizer'; import {renderToHtml} from './render_util'; @@ -18,9 +20,9 @@ describe('styling', () => { beforeEach(() => { element = {} as any; }); function initContext( - styles?: (number | string)[] | null, - classes?: (string | number | boolean)[] | null): StylingContext { - return allocStylingContext(element, createStylingContextTemplate(styles, classes)); + styles?: (number | string)[] | null, classes?: (string | number | boolean)[] | null, + sanitizer?: StyleSanitizeFn | null): StylingContext { + return allocStylingContext(element, createStylingContextTemplate(classes, styles, sanitizer)); } function renderStyles(context: StylingContext, renderer?: Renderer3) { @@ -55,14 +57,23 @@ describe('styling', () => { } function updateClasses(context: StylingContext, classes: string | {[key: string]: any} | null) { - updateStylingMap(context, null, classes); + updateStylingMap(context, classes, null); } - function cleanStyle(a: number = 0, b: number = 0): number { return _clean(a, b, false); } + function updateStyles(context: StylingContext, styles: {[key: string]: any} | null) { + updateStylingMap(context, null, styles); + } + + function cleanStyle(a: number = 0, b: number = 0): number { return _clean(a, b, false, false); } + + function cleanStyleWithSanitization(a: number = 0, b: number = 0): number { + return _clean(a, b, false, true); + } function cleanClass(a: number, b: number) { return _clean(a, b, true); } - function _clean(a: number = 0, b: number = 0, isClassBased: boolean): number { + function _clean( + a: number = 0, b: number = 0, isClassBased: boolean, sanitizable?: boolean): number { let num = 0; if (a) { num |= a << StylingFlags.BitCountSize; @@ -73,24 +84,32 @@ describe('styling', () => { if (isClassBased) { num |= StylingFlags.Class; } + if (sanitizable) { + num |= StylingFlags.Sanitize; + } return num; } - function _dirty(a: number = 0, b: number = 0, isClassBased: boolean): number { - return _clean(a, b, isClassBased) | StylingFlags.Dirty; + function _dirty( + a: number = 0, b: number = 0, isClassBased: boolean, sanitizable?: boolean): number { + return _clean(a, b, isClassBased, sanitizable) | StylingFlags.Dirty; } function dirtyStyle(a: number = 0, b: number = 0): number { return _dirty(a, b, false) | StylingFlags.Dirty; } + function dirtyStyleWithSanitization(a: number = 0, b: number = 0): number { + return _dirty(a, b, false, true); + } + function dirtyClass(a: number, b: number) { return _dirty(a, b, true); } describe('styles', () => { describe('createStylingContextTemplate', () => { it('should initialize empty template', () => { const template = initContext(); - expect(template).toEqual([element, [null], cleanStyle(0, 5), 0, null]); + expect(template).toEqual([element, null, [null], cleanStyle(0, 6), 0, null]); }); it('should initialize static styles', () => { @@ -98,28 +117,29 @@ describe('styling', () => { initContext([InitialStylingFlags.VALUES_MODE, 'color', 'red', 'width', '10px']); expect(template).toEqual([ element, + null, [null, 'red', '10px'], - dirtyStyle(0, 11), // + dirtyStyle(0, 12), // 0, null, - // #5 - cleanStyle(1, 11), + // #6 + cleanStyle(1, 12), 'color', null, - // #8 - cleanStyle(2, 14), + // #9 + cleanStyle(2, 15), 'width', null, - // #11 - dirtyStyle(1, 5), + // #12 + dirtyStyle(1, 6), 'color', null, - // #14 - dirtyStyle(2, 8), + // #15 + dirtyStyle(2, 9), 'width', null, ]); @@ -132,7 +152,7 @@ describe('styling', () => { function Template(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'span'); - elementStyling([ + elementStyling([], [ 'width', 'height', 'opacity', // InitialStylingFlags.VALUES_MODE, 'width', '100px', 'height', '100px', 'opacity', '0.5' @@ -160,30 +180,30 @@ describe('styling', () => { it('should build a list of multiple styling values', () => { const getStyles = trackStylesFactory(); const stylingContext = initContext(); - updateStylingMap(stylingContext, { + updateStyles(stylingContext, { width: '100px', height: '100px', }); - updateStylingMap(stylingContext, {height: '200px'}); + updateStyles(stylingContext, {height: '200px'}); expect(getStyles(stylingContext)).toEqual({width: null, height: '200px'}); }); it('should evaluate the delta between style changes when rendering occurs', () => { const stylingContext = initContext(['width', 'height', InitialStylingFlags.VALUES_MODE, 'width', '100px']); - updateStylingMap(stylingContext, { + updateStyles(stylingContext, { height: '200px', }); expect(renderStyles(stylingContext)).toEqual({width: '100px', height: '200px'}); expect(renderStyles(stylingContext)).toEqual({}); - updateStylingMap(stylingContext, { + updateStyles(stylingContext, { width: '100px', height: '100px', }); expect(renderStyles(stylingContext)).toEqual({height: '100px'}); updateStyleProp(stylingContext, 1, '100px'); expect(renderStyles(stylingContext)).toEqual({}); - updateStylingMap(stylingContext, { + updateStyles(stylingContext, { width: '100px', height: '100px', }); @@ -193,7 +213,7 @@ describe('styling', () => { it('should update individual values on a set of styles', () => { const getStyles = trackStylesFactory(); const stylingContext = initContext(['width', 'height']); - updateStylingMap(stylingContext, { + updateStyles(stylingContext, { width: '100px', height: '100px', }); @@ -205,7 +225,7 @@ describe('styling', () => { const stylingContext = initContext(); expect(isContextDirty(stylingContext)).toBeFalsy(); - updateStylingMap(stylingContext, { + updateStyles(stylingContext, { width: '100px', height: '100px', }); @@ -213,13 +233,13 @@ describe('styling', () => { setContextDirty(stylingContext, false); - updateStylingMap(stylingContext, { + updateStyles(stylingContext, { width: '100px', height: '100px', }); expect(isContextDirty(stylingContext)).toBeFalsy(); - updateStylingMap(stylingContext, { + updateStyles(stylingContext, { width: '200px', height: '100px', }); @@ -228,7 +248,7 @@ describe('styling', () => { it('should only mark itself as updated when any single properties have been applied', () => { const stylingContext = initContext(['height']); - updateStylingMap(stylingContext, { + updateStyles(stylingContext, { width: '100px', height: '100px', }); @@ -258,7 +278,7 @@ describe('styling', () => { opacity: '0', }); - updateStylingMap(stylingContext, {width: '200px', height: '200px'}); + updateStyles(stylingContext, {width: '200px', height: '200px'}); expect(getStyles(stylingContext)).toEqual({ width: '200px', @@ -282,7 +302,7 @@ describe('styling', () => { opacity: '0', }); - updateStylingMap(stylingContext, {}); + updateStyles(stylingContext, {}); expect(getStyles(stylingContext)).toEqual({ width: '100px', @@ -295,68 +315,70 @@ describe('styling', () => { const stylingContext = initContext(['width', 'height']); const getStyles = trackStylesFactory(); - updateStylingMap(stylingContext, {width: '100px', height: '100px'}); + updateStyles(stylingContext, {width: '100px', height: '100px'}); expect(stylingContext).toEqual([ element, + null, [null], - dirtyStyle(0, 11), // + dirtyStyle(0, 12), // 2, null, - // #5 - cleanStyle(0, 11), + // #6 + cleanStyle(0, 12), 'width', null, - // #8 - cleanStyle(0, 14), + // #9 + cleanStyle(0, 15), 'height', null, - // #11 - dirtyStyle(0, 5), + // #12 + dirtyStyle(0, 6), 'width', '100px', - // #14 - dirtyStyle(0, 8), + // #15 + dirtyStyle(0, 9), 'height', '100px', ]); getStyles(stylingContext); - updateStylingMap(stylingContext, {width: '200px', opacity: '0'}); + updateStyles(stylingContext, {width: '200px', opacity: '0'}); expect(stylingContext).toEqual([ element, + null, [null], - dirtyStyle(0, 11), // + dirtyStyle(0, 12), // 2, null, - // #5 - cleanStyle(0, 11), + // #6 + cleanStyle(0, 12), 'width', null, - // #8 - cleanStyle(0, 17), + // #9 + cleanStyle(0, 18), 'height', null, - // #11 - dirtyStyle(0, 5), + // #12 + dirtyStyle(0, 6), 'width', '200px', - // #14 + // #15 dirtyStyle(), 'opacity', '0', - // #17 - dirtyStyle(0, 8), + // #18 + dirtyStyle(0, 9), 'height', null, ]); @@ -364,69 +386,71 @@ describe('styling', () => { getStyles(stylingContext); expect(stylingContext).toEqual([ element, + null, [null], - cleanStyle(0, 11), // + cleanStyle(0, 12), // 2, null, - // #5 - cleanStyle(0, 11), + // #6 + cleanStyle(0, 12), 'width', null, - // #8 - cleanStyle(0, 17), + // #9 + cleanStyle(0, 18), 'height', null, - // #11 - cleanStyle(0, 5), + // #12 + cleanStyle(0, 6), 'width', '200px', - // #14 + // #15 cleanStyle(), 'opacity', '0', - // #17 - cleanStyle(0, 8), + // #18 + cleanStyle(0, 9), 'height', null, ]); - updateStylingMap(stylingContext, {width: null}); + updateStyles(stylingContext, {width: null}); updateStyleProp(stylingContext, 0, '300px'); expect(stylingContext).toEqual([ element, + null, [null], - dirtyStyle(0, 11), // + dirtyStyle(0, 12), // 2, null, - // #5 - dirtyStyle(0, 11), + // #6 + dirtyStyle(0, 12), 'width', '300px', - // #8 - cleanStyle(0, 17), + // #9 + cleanStyle(0, 18), 'height', null, - // #11 - cleanStyle(0, 5), + // #12 + cleanStyle(0, 6), 'width', null, - // #14 + // #15 dirtyStyle(), 'opacity', null, - // #17 - cleanStyle(0, 8), + // #18 + cleanStyle(0, 9), 'height', null, ]); @@ -436,33 +460,34 @@ describe('styling', () => { updateStyleProp(stylingContext, 0, null); expect(stylingContext).toEqual([ element, + null, [null], - dirtyStyle(0, 11), // + dirtyStyle(0, 12), // 2, null, - // #5 - dirtyStyle(0, 11), + // #6 + dirtyStyle(0, 12), 'width', null, - // #8 - cleanStyle(0, 17), + // #9 + cleanStyle(0, 18), 'height', null, - // #11 - cleanStyle(0, 5), + // #12 + cleanStyle(0, 6), 'width', null, - // #14 + // #15 cleanStyle(), 'opacity', null, - // #17 - cleanStyle(0, 8), + // #18 + cleanStyle(0, 9), 'height', null, ]); @@ -473,116 +498,119 @@ describe('styling', () => { const stylingContext = initContext(['lineHeight']); const getStyles = trackStylesFactory(); - updateStylingMap(stylingContext, {width: '100px', height: '100px', opacity: '0.5'}); + updateStyles(stylingContext, {width: '100px', height: '100px', opacity: '0.5'}); expect(stylingContext).toEqual([ element, + null, [null], - dirtyStyle(0, 8), // + dirtyStyle(0, 9), // 1, null, - // #5 - cleanStyle(0, 17), + // #6 + cleanStyle(0, 18), 'lineHeight', null, - // #8 + // #9 dirtyStyle(), 'width', '100px', - // #11 + // #12 dirtyStyle(), 'height', '100px', - // #14 + // #15 dirtyStyle(), 'opacity', '0.5', - // #17 - cleanStyle(0, 5), + // #18 + cleanStyle(0, 6), 'lineHeight', null, ]); getStyles(stylingContext); - updateStylingMap(stylingContext, {}); + updateStyles(stylingContext, {}); expect(stylingContext).toEqual([ element, + null, [null], - dirtyStyle(0, 8), // + dirtyStyle(0, 9), // 1, null, - // #5 - cleanStyle(0, 17), + // #6 + cleanStyle(0, 18), 'lineHeight', null, - // #8 + // #9 dirtyStyle(), 'width', null, - // #11 + // #12 dirtyStyle(), 'height', null, - // #14 + // #15 dirtyStyle(), 'opacity', null, - // #17 - cleanStyle(0, 5), + // #18 + cleanStyle(0, 6), 'lineHeight', null, ]); getStyles(stylingContext); - updateStylingMap(stylingContext, { + updateStyles(stylingContext, { borderWidth: '5px', }); expect(stylingContext).toEqual([ element, + null, [null], - dirtyStyle(0, 8), // + dirtyStyle(0, 9), // 1, null, - // #5 - cleanStyle(0, 20), + // #6 + cleanStyle(0, 21), 'lineHeight', null, - // #8 + // #9 dirtyStyle(), 'borderWidth', '5px', - // #11 + // #12 cleanStyle(), 'width', null, - // #14 + // #15 cleanStyle(), 'height', null, - // #17 + // #18 cleanStyle(), 'opacity', null, - // #20 - cleanStyle(0, 5), + // #21 + cleanStyle(0, 6), 'lineHeight', null, ]); @@ -591,83 +619,85 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, + null, [null], - dirtyStyle(0, 8), // + dirtyStyle(0, 9), // 1, null, - // #5 - dirtyStyle(0, 20), + // #6 + dirtyStyle(0, 21), 'lineHeight', '200px', - // #8 + // #9 dirtyStyle(), 'borderWidth', '5px', - // #11 + // #12 cleanStyle(), 'width', null, - // #14 + // #15 cleanStyle(), 'height', null, - // #17 + // #18 cleanStyle(), 'opacity', null, - // #20 - cleanStyle(0, 5), + // #21 + cleanStyle(0, 6), 'lineHeight', null, ]); - updateStylingMap(stylingContext, {borderWidth: '15px', borderColor: 'red'}); + updateStyles(stylingContext, {borderWidth: '15px', borderColor: 'red'}); expect(stylingContext).toEqual([ element, + null, [null], - dirtyStyle(0, 8), // + dirtyStyle(0, 9), // 1, null, - // #5 - dirtyStyle(0, 23), + // #6 + dirtyStyle(0, 24), 'lineHeight', '200px', - // #8 + // #9 dirtyStyle(), 'borderWidth', '15px', - // #11 + // #12 dirtyStyle(), 'borderColor', 'red', - // #14 + // #15 cleanStyle(), 'width', null, - // #17 + // #18 cleanStyle(), 'height', null, - // #20 + // #21 cleanStyle(), 'opacity', null, - // #23 - cleanStyle(0, 5), + // #24 + cleanStyle(0, 6), 'lineHeight', null, ]); @@ -677,7 +707,7 @@ describe('styling', () => { const getStyles = trackStylesFactory(); const stylingContext = initContext(['height']); - updateStylingMap(stylingContext, { + updateStyles(stylingContext, { width: '100px', }); @@ -685,23 +715,24 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, + null, [null], - dirtyStyle(0, 8), // + dirtyStyle(0, 9), // 1, null, - // #5 - dirtyStyle(0, 11), + // #6 + dirtyStyle(0, 12), 'height', '200px', - // #5 + // #6 dirtyStyle(), 'width', '100px', - // #11 - cleanStyle(0, 5), + // #12 + cleanStyle(0, 6), 'height', null, ]); @@ -710,27 +741,140 @@ describe('styling', () => { expect(stylingContext).toEqual([ element, + null, [null], - cleanStyle(0, 8), // + cleanStyle(0, 9), // 1, null, - // #5 - cleanStyle(0, 11), + // #6 + cleanStyle(0, 12), 'height', '200px', - // #5 + // #6 cleanStyle(), 'width', '100px', - // #11 - cleanStyle(0, 5), + // #12 + cleanStyle(0, 6), 'height', null, ]); }); + + it('should mark styles that may contain url values as being sanitizable (when a sanitizer is passed in)', + () => { + const getStyles = trackStylesFactory(); + const initialStyles = ['border-image', 'border-width']; + const styleSanitizer = defaultStyleSanitizer; + const stylingContext = initContext(initialStyles, null, styleSanitizer); + + updateStyleProp(stylingContext, 0, 'url(foo.jpg)'); + updateStyleProp(stylingContext, 1, '100px'); + + expect(stylingContext).toEqual([ + element, + styleSanitizer, + [null], + dirtyStyle(0, 12), // + 2, + null, + + // #6 + dirtyStyleWithSanitization(0, 12), + 'border-image', + 'url(foo.jpg)', + + // #9 + dirtyStyle(0, 15), + 'border-width', + '100px', + + // #12 + cleanStyleWithSanitization(0, 6), + 'border-image', + null, + + // #15 + cleanStyle(0, 9), + 'border-width', + null, + ]); + + updateStyles(stylingContext, {'background-image': 'unsafe'}); + + expect(stylingContext).toEqual([ + element, + styleSanitizer, + [null], + dirtyStyle(0, 12), // + 2, + null, + + // #6 + dirtyStyleWithSanitization(0, 15), + 'border-image', + 'url(foo.jpg)', + + // #9 + dirtyStyle(0, 18), + 'border-width', + '100px', + + // #12 + dirtyStyleWithSanitization(0, 0), + 'background-image', + 'unsafe', + + // #15 + cleanStyleWithSanitization(0, 6), + 'border-image', + null, + + // #18 + cleanStyle(0, 9), + 'border-width', + null, + ]); + + getStyles(stylingContext); + + expect(stylingContext).toEqual([ + element, + styleSanitizer, + [null], + cleanStyle(0, 12), // + 2, + null, + + // #6 + cleanStyleWithSanitization(0, 15), + 'border-image', + 'url(foo.jpg)', + + // #9 + cleanStyle(0, 18), + 'border-width', + '100px', + + // #12 + cleanStyleWithSanitization(0, 0), + 'background-image', + 'unsafe', + + // #15 + cleanStyleWithSanitization(0, 6), + 'border-image', + null, + + // #18 + cleanStyle(0, 9), + 'border-width', + null, + ]); + }); }); }); @@ -739,20 +883,20 @@ describe('styling', () => { const template = initContext(null, [InitialStylingFlags.VALUES_MODE, 'one', true, 'two', true]); expect(template).toEqual([ - element, [null, true, true], dirtyStyle(0, 11), // + element, null, [null, true, true], dirtyStyle(0, 12), // 0, null, - // #5 - cleanClass(1, 11), 'one', null, + // #6 + cleanClass(1, 12), 'one', null, - // #8 - cleanClass(2, 14), 'two', null, + // #9 + cleanClass(2, 15), 'two', null, - // #11 - dirtyClass(1, 5), 'one', null, + // #12 + dirtyClass(1, 6), 'one', null, - // #14 - dirtyClass(2, 8), 'two', null + // #15 + dirtyClass(2, 9), 'two', null ]); }); @@ -787,10 +931,10 @@ describe('styling', () => { const stylingContext = initContext(null, ['guy']); expect(getClasses(stylingContext)).toEqual({}); - updateStylingMap(stylingContext, null, 'foo bar guy'); + updateStylingMap(stylingContext, 'foo bar guy'); expect(getClasses(stylingContext)).toEqual({'foo': true, 'bar': true, 'guy': true}); - updateStylingMap(stylingContext, null, 'foo man'); + updateStylingMap(stylingContext, 'foo man'); updateClassProp(stylingContext, 0, true); expect(getClasses(stylingContext)) .toEqual({'foo': true, 'man': true, 'bar': false, 'guy': true}); @@ -803,109 +947,111 @@ describe('styling', () => { const stylingContext = initContext(initialStyles, initialClasses); expect(stylingContext).toEqual([ element, + null, [null, '100px', true], - dirtyStyle(0, 17), // + dirtyStyle(0, 18), // 2, null, - // #5 - cleanStyle(1, 17), + // #6 + cleanStyle(1, 18), 'width', null, - // #8 - cleanStyle(0, 20), + // #9 + cleanStyle(0, 21), 'height', null, - // #11 - cleanClass(2, 23), + // #12 + cleanClass(2, 24), 'wide', null, - // #14 - cleanClass(0, 26), + // #15 + cleanClass(0, 27), 'tall', null, - // #17 - dirtyStyle(1, 5), + // #18 + dirtyStyle(1, 6), 'width', null, - // #20 - cleanStyle(0, 8), + // #21 + cleanStyle(0, 9), 'height', null, - // #23 - dirtyClass(2, 11), + // #24 + dirtyClass(2, 12), 'wide', null, - // #26 - cleanClass(0, 14), + // #27 + cleanClass(0, 15), 'tall', null, ]); expect(getStylesAndClasses(stylingContext)).toEqual([{width: '100px'}, {wide: true}]); - updateStylingMap(stylingContext, {width: '200px', opacity: '0.5'}, 'tall round'); + updateStylingMap(stylingContext, 'tall round', {width: '200px', opacity: '0.5'}); expect(stylingContext).toEqual([ element, + null, [null, '100px', true], - dirtyStyle(0, 17), // + dirtyStyle(0, 18), // 2, 'tall round', - // #5 - cleanStyle(1, 17), + // #6 + cleanStyle(1, 18), 'width', null, - // #8 - cleanStyle(0, 32), + // #9 + cleanStyle(0, 33), 'height', null, - // #11 - cleanClass(2, 29), + // #12 + cleanClass(2, 30), 'wide', null, - // #14 - cleanClass(0, 23), + // #15 + cleanClass(0, 24), 'tall', null, - // #17 - dirtyStyle(1, 5), + // #18 + dirtyStyle(1, 6), 'width', '200px', - // #20 + // #21 dirtyStyle(0, 0), 'opacity', '0.5', - // #23 - dirtyClass(0, 14), + // #24 + dirtyClass(0, 15), 'tall', true, - // #26 + // #27 dirtyClass(0, 0), 'round', true, - // #29 - cleanClass(2, 11), + // #30 + cleanClass(2, 12), 'wide', null, - // #32 - cleanStyle(0, 8), + // #33 + cleanStyle(0, 9), 'height', null, ]); @@ -914,63 +1060,64 @@ describe('styling', () => { {width: '200px', opacity: '0.5'}, {tall: true, round: true, wide: true} ]); - updateStylingMap(stylingContext, {width: '500px'}, {tall: true, wide: true}); + updateStylingMap(stylingContext, {tall: true, wide: true}, {width: '500px'}); updateStyleProp(stylingContext, 0, '300px'); expect(stylingContext).toEqual([ element, + null, [null, '100px', true], - dirtyStyle(0, 17), // + dirtyStyle(0, 18), // 2, null, - // #5 - dirtyStyle(1, 17), + // #6 + dirtyStyle(1, 18), 'width', '300px', - // #8 - cleanStyle(0, 32), + // #9 + cleanStyle(0, 33), 'height', null, - // #11 - cleanClass(2, 23), + // #12 + cleanClass(2, 24), 'wide', null, - // #14 - cleanClass(0, 20), + // #15 + cleanClass(0, 21), 'tall', null, - // #17 - cleanStyle(1, 5), + // #18 + cleanStyle(1, 6), 'width', '500px', - // #20 - cleanClass(0, 14), + // #21 + cleanClass(0, 15), 'tall', true, - // #23 - cleanClass(2, 11), + // #24 + cleanClass(2, 12), 'wide', true, - // #26 + // #27 dirtyClass(0, 0), 'round', null, - // #29 + // #30 dirtyStyle(0, 0), 'opacity', null, - // #32 - cleanStyle(0, 8), + // #33 + cleanStyle(0, 9), 'height', null, ]); diff --git a/packages/core/test/sanitization/sanatization_spec.ts b/packages/core/test/sanitization/sanatization_spec.ts index 6c9f31e8c0..c1663f4aff 100644 --- a/packages/core/test/sanitization/sanatization_spec.ts +++ b/packages/core/test/sanitization/sanatization_spec.ts @@ -7,7 +7,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization'; +import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl} from '../../src/sanitization/bypass'; +import {sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization'; describe('sanitization', () => { class Wrap { @@ -64,4 +65,4 @@ describe('sanitization', () => { expect(() => sanitizeScript(bypassSanitizationTrustHtml('true'))).toThrowError(ERROR); expect(sanitizeScript(bypassSanitizationTrustScript('true'))).toEqual('true'); }); -}); \ No newline at end of file +});