diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/style_bindings/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/style_bindings/TEST_CASES.json index 5812b619f7a..b59b4c27398 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/style_bindings/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/style_bindings/TEST_CASES.json @@ -3,88 +3,63 @@ "cases": [ { "description": "should create style instructions on the element", - "inputFiles": [ - "style_binding.ts" - ], + "inputFiles": ["style_binding.ts"], "expectations": [ { "failureMessage": "Incorrect template", - "files": [ - "style_binding.js" - ] + "files": ["style_binding.js"] } ] }, { "description": "should correctly count the total slots required when style/class bindings include interpolation", - "inputFiles": [ - "binding_slots.ts" - ], + "inputFiles": ["binding_slots.ts"], "expectations": [ { "failureMessage": "Incorrect template", - "files": [ - "binding_slots.js" - ] + "files": ["binding_slots.js"] } ] }, { "description": "should place initial, multi, singular and application followed by attribute style instructions in the template code in that order", - "inputFiles": [ - "binding_slots_interpolations.ts" - ], + "inputFiles": ["binding_slots_interpolations.ts"], "expectations": [ { "failureMessage": "Incorrect template", - "files": [ - "binding_slots_interpolations.js" - ] + "files": ["binding_slots_interpolations.js"] } - ], - "skipForTemplatePipeline": true + ] }, { "description": "should assign a sanitizer instance to the element style allocation instruction if any url-based properties are detected", - "inputFiles": [ - "style_ordering.ts" - ], + "inputFiles": ["style_ordering.ts"], "expectations": [ { "failureMessage": "Incorrect template", - "files": [ - "style_ordering.js" - ] + "files": ["style_ordering.js"] } ] }, { "description": "should support [style.foo.suffix] style bindings with a suffix", - "inputFiles": [ - "style_binding_suffixed.ts" - ], + "inputFiles": ["style_binding_suffixed.ts"], "expectations": [ { "failureMessage": "Incorrect template", - "files": [ - "style_binding_suffixed.js" - ] + "files": ["style_binding_suffixed.js"] } ] }, { "description": "should not create instructions for empty style bindings", - "inputFiles": [ - "empty_style_bindings.ts" - ], + "inputFiles": ["empty_style_bindings.ts"], "expectations": [ { "failureMessage": "Incorrect template", - "files": [ - "empty_style_bindings.js" - ] + "files": ["empty_style_bindings.js"] } ] } ] -} \ No newline at end of file +} diff --git a/packages/compiler/src/template/pipeline/ir/src/enums.ts b/packages/compiler/src/template/pipeline/ir/src/enums.ts index 5084c9d62e1..a8ee9259e5c 100644 --- a/packages/compiler/src/template/pipeline/ir/src/enums.ts +++ b/packages/compiler/src/template/pipeline/ir/src/enums.ts @@ -246,6 +246,11 @@ export enum ExpressionKind { * A reference to a temporary variable. */ ReadTemporaryExpr, + + /** + * An expression representing a sanitizer function. + */ + SanitizerExpr, } /** @@ -277,3 +282,15 @@ export enum CompatibilityMode { Normal, TemplateDefinitionBuilder, } + +/** + * Represents functions used to sanitize different pieces of a template. + */ +export enum SanitizerFn { + Html, + Script, + Style, + Url, + ResourceUrl, + IframeAttribute, +} diff --git a/packages/compiler/src/template/pipeline/ir/src/expression.ts b/packages/compiler/src/template/pipeline/ir/src/expression.ts index 85bd05e784e..025c6102a1e 100644 --- a/packages/compiler/src/template/pipeline/ir/src/expression.ts +++ b/packages/compiler/src/template/pipeline/ir/src/expression.ts @@ -9,7 +9,7 @@ import * as o from '../../../../output/output_ast'; import type {ParseSourceSpan} from '../../../../parse_util'; -import {ExpressionKind, OpKind} from './enums'; +import {ExpressionKind, OpKind, SanitizerFn} from './enums'; import {ConsumesVarsTrait, UsesSlotIndex, UsesSlotIndexTrait, UsesVarOffset, UsesVarOffsetTrait} from './traits'; import type {XrefId} from './operations'; @@ -19,10 +19,11 @@ import {Interpolation, type UpdateOp} from './ops/update'; /** * An `o.Expression` subtype representing a logical expression in the intermediate representation. */ -export type Expression = LexicalReadExpr|ReferenceExpr|ContextExpr|NextContextExpr| - GetCurrentViewExpr|RestoreViewExpr|ResetViewExpr|ReadVariableExpr|PureFunctionExpr| - PureFunctionParameterExpr|PipeBindingExpr|PipeBindingVariadicExpr|SafePropertyReadExpr| - SafeKeyedReadExpr|SafeInvokeFunctionExpr|EmptyExpr|AssignTemporaryExpr|ReadTemporaryExpr; +export type Expression = + LexicalReadExpr|ReferenceExpr|ContextExpr|NextContextExpr|GetCurrentViewExpr|RestoreViewExpr| + ResetViewExpr|ReadVariableExpr|PureFunctionExpr|PureFunctionParameterExpr|PipeBindingExpr| + PipeBindingVariadicExpr|SafePropertyReadExpr|SafeKeyedReadExpr|SafeInvokeFunctionExpr|EmptyExpr| + AssignTemporaryExpr|ReadTemporaryExpr|SanitizerExpr; /** * Transformer type which converts expressions into general `o.Expression`s (which may be an @@ -709,6 +710,30 @@ export class ReadTemporaryExpr extends ExpressionBase { } } +export class SanitizerExpr extends ExpressionBase { + override readonly kind = ExpressionKind.SanitizerExpr; + + constructor(public fn: SanitizerFn) { + super(); + } + + override visitExpression(visitor: o.ExpressionVisitor, context: any): any {} + + override isEquivalent(e: Expression): boolean { + return e instanceof SanitizerExpr && e.fn === this.fn; + } + + override isConstant() { + return true; + } + + override clone(): SanitizerExpr { + return new SanitizerExpr(this.fn); + } + + override transformInternalExpressions(): void {} +} + /** * Visits all `Expression`s in the AST of `op` with the `visitor` function. */ @@ -742,12 +767,10 @@ function transformExpressionsInInterpolation( export function transformExpressionsInOp( op: CreateOp|UpdateOp, transform: ExpressionTransform, flags: VisitorContextFlag): void { switch (op.kind) { - case OpKind.Property: case OpKind.StyleProp: case OpKind.StyleMap: case OpKind.ClassProp: case OpKind.ClassMap: - case OpKind.Attribute: case OpKind.Binding: case OpKind.HostProperty: if (op.expression instanceof Interpolation) { @@ -756,6 +779,16 @@ export function transformExpressionsInOp( op.expression = transformExpressionsInExpression(op.expression, transform, flags); } break; + case OpKind.Property: + case OpKind.Attribute: + if (op.expression instanceof Interpolation) { + transformExpressionsInInterpolation(op.expression, transform, flags); + } else { + op.expression = transformExpressionsInExpression(op.expression, transform, flags); + } + op.sanitizer = + op.sanitizer && transformExpressionsInExpression(op.sanitizer, transform, flags); + break; case OpKind.InterpolateText: transformExpressionsInInterpolation(op.interpolation, transform, flags); break; diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/update.ts b/packages/compiler/src/template/pipeline/ir/src/ops/update.ts index c115f4214ef..f7c555fc959 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/update.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/update.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {SecurityContext} from '../../../../../core'; import * as o from '../../../../../output/output_ast'; import {ParseSourceSpan} from '../../../../../parse_util'; import {BindingKind} from '../element'; @@ -71,17 +72,42 @@ export class Interpolation { export interface BindingOp extends Op { kind: OpKind.Binding; + /** + * Reference to the element on which the property is bound. + */ target: XrefId; + /** + * The kind of binding represented by this op. + */ bindingKind: BindingKind; + + /** + * The name of this binding. + */ name: string; + + /** + * Expression which is bound to the property. + */ expression: o.Expression|Interpolation; - sourceSpan: ParseSourceSpan; - isTemplate: boolean; + /** * The unit of the bound value. */ unit: string|null; + + /** + * The security context of the binding. + */ + securityContext: SecurityContext; + + /** + * Whether this binding is on a template. + */ + isTemplate: boolean; + + sourceSpan: ParseSourceSpan; } /** @@ -89,7 +115,8 @@ export interface BindingOp extends Op { */ export function createBindingOp( target: XrefId, kind: BindingKind, name: string, expression: o.Expression|Interpolation, - unit: string|null, isTemplate: boolean, sourceSpan: ParseSourceSpan): BindingOp { + unit: string|null, securityContext: SecurityContext, isTemplate: boolean, + sourceSpan: ParseSourceSpan): BindingOp { return { kind: OpKind.Binding, bindingKind: kind, @@ -97,6 +124,7 @@ export function createBindingOp( name, expression, unit, + securityContext, isTemplate, sourceSpan, ...NEW_OP, @@ -129,6 +157,19 @@ export interface PropertyOp extends Op, ConsumesVarsTrait, DependsOnSl */ isAnimationTrigger: boolean; + /** + * The security context of the binding. + */ + securityContext: SecurityContext; + + /** + * The sanitizer for this property. + */ + sanitizer: o.Expression|null; + + /** + * Whether this binding is on a template. + */ isTemplate: boolean; sourceSpan: ParseSourceSpan; @@ -139,7 +180,7 @@ export interface PropertyOp extends Op, ConsumesVarsTrait, DependsOnSl */ export function createPropertyOp( target: XrefId, name: string, expression: o.Expression|Interpolation, - isAnimationTrigger: boolean, isTemplate: boolean, + isAnimationTrigger: boolean, securityContext: SecurityContext, isTemplate: boolean, sourceSpan: ParseSourceSpan): PropertyOp { return { @@ -148,6 +189,8 @@ export function createPropertyOp( name, expression, isAnimationTrigger, + securityContext, + sanitizer: null, isTemplate, sourceSpan, ...TRAIT_DEPENDS_ON_SLOT_CONTEXT, @@ -333,6 +376,19 @@ export interface AttributeOp extends Op { */ expression: o.Expression|Interpolation; + /** + * The security context of the binding. + */ + securityContext: SecurityContext; + + /** + * The sanitizer for this attribute. + */ + sanitizer: o.Expression|null; + + /** + * Whether this binding is on a template. + */ isTemplate: boolean; sourceSpan: ParseSourceSpan; @@ -342,13 +398,16 @@ export interface AttributeOp extends Op { * Create an `AttributeOp`. */ export function createAttributeOp( - target: XrefId, name: string, expression: o.Expression|Interpolation, isTemplate: boolean, + target: XrefId, name: string, expression: o.Expression|Interpolation, + securityContext: SecurityContext, isTemplate: boolean, sourceSpan: ParseSourceSpan): AttributeOp { return { kind: OpKind.Attribute, target, name, expression, + securityContext, + sanitizer: null, isTemplate, sourceSpan, ...TRAIT_DEPENDS_ON_SLOT_CONTEXT, diff --git a/packages/compiler/src/template/pipeline/src/emit.ts b/packages/compiler/src/template/pipeline/src/emit.ts index bad03921ce1..14c12eaf820 100644 --- a/packages/compiler/src/template/pipeline/src/emit.ts +++ b/packages/compiler/src/template/pipeline/src/emit.ts @@ -10,20 +10,26 @@ import * as o from '../../../../src/output/output_ast'; import {ConstantPool} from '../../../constant_pool'; import * as ir from '../ir'; -import type {ComponentCompilationJob, ViewCompilationUnit, HostBindingCompilationJob} from './compilation'; +import type {ComponentCompilationJob, HostBindingCompilationJob, ViewCompilationUnit} from './compilation'; import {phaseAlignPipeVariadicVarOffset} from './phases/align_pipe_variadic_var_offset'; +import {phaseFindAnyCasts} from './phases/any_cast'; import {phaseAttributeExtraction} from './phases/attribute_extraction'; +import {phaseBindingSpecialization} from './phases/binding_specialization'; import {phaseChaining} from './phases/chaining'; import {phaseConstCollection} from './phases/const_collection'; import {phaseEmptyElements} from './phases/empty_elements'; import {phaseExpandSafeReads} from './phases/expand_safe_reads'; import {phaseGenerateAdvance} from './phases/generate_advance'; import {phaseGenerateVariables} from './phases/generate_variables'; +import {phaseHostStylePropertyParsing} from './phases/host_style_property_parsing'; import {phaseLocalRefs} from './phases/local_refs'; +import {phaseNamespace} from './phases/namespace'; import {phaseNaming} from './phases/naming'; import {phaseMergeNextContext} from './phases/next_context_merging'; import {phaseNgContainer} from './phases/ng_container'; +import {phaseNoListenersOnTemplates} from './phases/no_listeners_on_templates'; +import {phaseNonbindable} from './phases/nonbindable'; import {phaseNullishCoalescing} from './phases/nullish_coalescing'; import {phasePipeCreation} from './phases/pipe_creation'; import {phasePipeVariadic} from './phases/pipe_variadic'; @@ -31,22 +37,17 @@ import {phasePropertyOrdering} from './phases/property_ordering'; import {phasePureFunctionExtraction} from './phases/pure_function_extraction'; import {phasePureLiteralStructures} from './phases/pure_literal_structures'; import {phaseReify} from './phases/reify'; +import {phaseRemoveEmptyBindings} from './phases/remove_empty_bindings'; import {phaseResolveContexts} from './phases/resolve_contexts'; +import {phaseResolveDollarEvent} from './phases/resolve_dollar_event'; import {phaseResolveNames} from './phases/resolve_names'; +import {phaseResolveSanitizers} from './phases/resolve_sanitizers'; import {phaseSaveRestoreView} from './phases/save_restore_view'; import {phaseSlotAllocation} from './phases/slot_allocation'; +import {phaseStyleBindingSpecialization} from './phases/style_binding_specialization'; import {phaseTemporaryVariables} from './phases/temporary_variables'; import {phaseVarCounting} from './phases/var_counting'; import {phaseVariableOptimization} from './phases/variable_optimization'; -import {phaseFindAnyCasts} from './phases/any_cast'; -import {phaseResolveDollarEvent} from './phases/resolve_dollar_event'; -import {phaseBindingSpecialization} from './phases/binding_specialization'; -import {phaseStyleBindingSpecialization} from './phases/style_binding_specialization'; -import {phaseRemoveEmptyBindings} from './phases/remove_empty_bindings'; -import {phaseNoListenersOnTemplates} from './phases/no_listeners_on_templates'; -import {phaseHostStylePropertyParsing} from './phases/host_style_property_parsing'; -import {phaseNonbindable} from './phases/nonbindable'; -import {phaseNamespace} from './phases/namespace'; /** * Run all transformation phases in the correct order against a `ComponentCompilation`. After this @@ -68,6 +69,7 @@ export function transformTemplate(job: ComponentCompilationJob): void { phaseResolveDollarEvent(job); phaseResolveNames(job); phaseResolveContexts(job); + phaseResolveSanitizers(job); phaseLocalRefs(job); phaseConstCollection(job); phaseNullishCoalescing(job); @@ -105,6 +107,8 @@ export function transformHostBinding(job: HostBindingCompilationJob): void { phaseVariableOptimization(job); phaseResolveNames(job); phaseResolveContexts(job); + // TODO: Figure out how to make this work for host bindings. + // phaseResolveSanitizers(job); phaseNaming(job); phasePureFunctionExtraction(job); phasePropertyOrdering(job); diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index d08fe051231..fb63ec88df4 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -7,6 +7,7 @@ */ import {ConstantPool} from '../../../constant_pool'; +import {SecurityContext} from '../../../core'; import * as e from '../../../expression_parser/ast'; import {splitNsName} from '../../../ml_parser/tags'; import * as o from '../../../output/output_ast'; @@ -74,7 +75,10 @@ export function ingestHostProperty( bindingKind = ir.BindingKind.Attribute; } job.update.push(ir.createBindingOp( - job.root.xref, bindingKind, property.name, expression, null, false, property.sourceSpan)); + job.root.xref, bindingKind, property.name, expression, null, + SecurityContext + .NONE /* TODO: what should we pass as security context? Passing NONE for now. */, + false, property.sourceSpan)); } export function ingestHostEvent(job: HostBindingCompilationJob, event: e.ParsedEvent) {} @@ -273,10 +277,11 @@ function ingestBindings( if (attr instanceof t.TextAttribute) { ingestBinding( view, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null, - attr.sourceSpan, true); + SecurityContext.NONE, attr.sourceSpan, true); } else { ingestBinding( - view, op.xref, attr.name, attr.value, attr.type, attr.unit, attr.sourceSpan, true); + view, op.xref, attr.name, attr.value, attr.type, attr.unit, attr.securityContext, + attr.sourceSpan, true); } } } @@ -287,12 +292,13 @@ function ingestBindings( // `BindingType.Attribute`. ingestBinding( view, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null, - attr.sourceSpan, false); + SecurityContext.NONE, attr.sourceSpan, false); } for (const input of element.inputs) { ingestBinding( - view, op.xref, input.name, input.value, input.type, input.unit, input.sourceSpan, false); + view, op.xref, input.name, input.value, input.type, input.unit, input.securityContext, + input.sourceSpan, false); } for (const output of element.outputs) { @@ -345,8 +351,8 @@ const BINDING_KINDS = new Map([ function ingestBinding( view: ViewCompilationUnit, xref: ir.XrefId, name: string, value: e.AST|o.Expression, - type: e.BindingType, unit: string|null, sourceSpan: ParseSourceSpan, - isTemplateBinding: boolean): void { + type: e.BindingType, unit: string|null, securityContext: SecurityContext, + sourceSpan: ParseSourceSpan, isTemplateBinding: boolean): void { if (value instanceof e.ASTWithSource) { value = value.ast; } @@ -362,8 +368,8 @@ function ingestBinding( } const kind: ir.BindingKind = BINDING_KINDS.get(type)!; - view.update.push( - ir.createBindingOp(xref, kind, name, expression, unit, isTemplateBinding, sourceSpan)); + view.update.push(ir.createBindingOp( + xref, kind, name, expression, unit, securityContext, isTemplateBinding, sourceSpan)); } /** diff --git a/packages/compiler/src/template/pipeline/src/instruction.ts b/packages/compiler/src/template/pipeline/src/instruction.ts index 2923973c2f2..4744183d53c 100644 --- a/packages/compiler/src/template/pipeline/src/instruction.ts +++ b/packages/compiler/src/template/pipeline/src/instruction.ts @@ -175,18 +175,22 @@ export function text( } export function property( - name: string, expression: o.Expression, sourceSpan: ParseSourceSpan): ir.UpdateOp { - return call( - Identifiers.property, - [ - o.literal(name), - expression, - ], - sourceSpan); + name: string, expression: o.Expression, sanitizer: o.Expression|null, + sourceSpan: ParseSourceSpan): ir.UpdateOp { + const args = [o.literal(name), expression]; + if (sanitizer !== null) { + args.push(sanitizer); + } + return call(Identifiers.property, args, sourceSpan); } -export function attribute(name: string, expression: o.Expression): ir.UpdateOp { - return call(Identifiers.attribute, [o.literal(name), expression], null); +export function attribute( + name: string, expression: o.Expression, sanitizer: o.Expression|null): ir.UpdateOp { + const args = [o.literal(name), expression]; + if (sanitizer !== null) { + args.push(sanitizer); + } + return call(Identifiers.attribute, args, null); } export function styleProp(name: string, expression: o.Expression, unit: string|null): ir.UpdateOp { @@ -261,20 +265,29 @@ export function textInterpolate( export function propertyInterpolate( - name: string, strings: string[], expressions: o.Expression[], + name: string, strings: string[], expressions: o.Expression[], sanitizer: o.Expression|null, sourceSpan: ParseSourceSpan): ir.UpdateOp { const interpolationArgs = collateInterpolationArgs(strings, expressions); + const extraArgs = []; + if (sanitizer !== null) { + extraArgs.push(sanitizer); + } return callVariadicInstruction( - PROPERTY_INTERPOLATE_CONFIG, [o.literal(name)], interpolationArgs, [], sourceSpan); + PROPERTY_INTERPOLATE_CONFIG, [o.literal(name)], interpolationArgs, extraArgs, sourceSpan); } export function attributeInterpolate( - name: string, strings: string[], expressions: o.Expression[]): ir.UpdateOp { + name: string, strings: string[], expressions: o.Expression[], + sanitizer: o.Expression|null): ir.UpdateOp { const interpolationArgs = collateInterpolationArgs(strings, expressions); + const extraArgs = []; + if (sanitizer !== null) { + extraArgs.push(sanitizer); + } return callVariadicInstruction( - ATTRIBUTE_INTERPOLATE_CONFIG, [o.literal(name)], interpolationArgs, [], null); + ATTRIBUTE_INTERPOLATE_CONFIG, [o.literal(name)], interpolationArgs, extraArgs, null); } export function stylePropInterpolate( @@ -350,8 +363,8 @@ function call( } /** - * Describes a specific flavor of instruction used to represent variadic instructions, which have - * some number of variants for specific argument counts. + * Describes a specific flavor of instruction used to represent variadic instructions, which + * have some number of variants for specific argument counts. */ interface VariadicInstructionConfig { constant: o.ExternalReference[]; diff --git a/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts b/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts index 7644629cdd3..22cbf32b9ae 100644 --- a/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts +++ b/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts @@ -6,9 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ +import {SecurityContext} from '../../../../core'; import * as o from '../../../../output/output_ast'; +import {parse as parseStyle} from '../../../../render3/view/style_parser'; import * as ir from '../../ir'; import {ComponentCompilationJob, ViewCompilationUnit} from '../compilation'; +import {getElementsByXrefId} from '../util/elements'; /** * Find all attribute and binding ops, and collect them into the ElementAttribute structures. @@ -37,38 +40,13 @@ function lookupElement( * not need further processing. */ function populateElementAttributes(view: ViewCompilationUnit) { - const elements = new Map(); - for (const op of view.create) { - if (!ir.isElementOrContainerOp(op)) { - continue; - } - elements.set(op.xref, op); - } + const elements = getElementsByXrefId(view); for (const op of view.ops()) { let ownerOp: ReturnType; switch (op.kind) { case ir.OpKind.Attribute: - ownerOp = lookupElement(elements, op.target); - ir.assertIsElementAttributes(ownerOp.attributes); - - if (op.expression instanceof ir.Interpolation) { - continue; - } - - // The old compiler only extracted string constants, so we emulate that behavior in - // compaitiblity mode, otherwise we optimize more aggressively. - let extractable = view.compatibility === ir.CompatibilityMode.TemplateDefinitionBuilder ? - (op.expression instanceof o.LiteralExpr && typeof op.expression.value === 'string') : - op.expression.isConstant(); - - // We don't need to generate instructions for attributes that can be extracted as consts. - if (extractable) { - ownerOp.attributes.add( - op.isTemplate ? ir.BindingKind.Template : ir.BindingKind.Attribute, op.name, - op.expression); - ir.OpList.remove(op as ir.UpdateOp); - } + extractAttributeOp(view, op, elements); break; case ir.OpKind.Property: if (op.isAnimationTrigger) { @@ -108,3 +86,47 @@ function populateElementAttributes(view: ViewCompilationUnit) { } } } + +function isStringLiteral(expr: o.Expression): expr is o.LiteralExpr&{value: string} { + return expr instanceof o.LiteralExpr && typeof expr.value === 'string'; +} + +function extractAttributeOp( + view: ViewCompilationUnit, op: ir.AttributeOp, + elements: Map) { + if (op.expression instanceof ir.Interpolation) { + return; + } + const ownerOp = lookupElement(elements, op.target); + ir.assertIsElementAttributes(ownerOp.attributes); + + if (op.name === 'style' && isStringLiteral(op.expression)) { + // TemplateDefinitionBuilder did not extract style attributes that had a security context. + if (view.compatibility === ir.CompatibilityMode.TemplateDefinitionBuilder && + op.securityContext !== SecurityContext.NONE) { + return; + } + + // Extract style attributes. + const parsedStyles = parseStyle(op.expression.value); + for (let i = 0; i < parsedStyles.length - 1; i += 2) { + ownerOp.attributes.add( + ir.BindingKind.StyleProperty, parsedStyles[i], o.literal(parsedStyles[i + 1])); + } + ir.OpList.remove(op as ir.UpdateOp); + } else { + // The old compiler only extracted string constants, so we emulate that behavior in + // compaitiblity mode, otherwise we optimize more aggressively. + let extractable = view.compatibility === ir.CompatibilityMode.TemplateDefinitionBuilder ? + (op.expression instanceof o.LiteralExpr && typeof op.expression.value === 'string') : + op.expression.isConstant(); + + // We don't need to generate instructions for attributes that can be extracted as consts. + if (extractable) { + ownerOp.attributes.add( + op.isTemplate ? ir.BindingKind.Template : ir.BindingKind.Attribute, op.name, + op.expression); + ir.OpList.remove(op as ir.UpdateOp); + } + } +} diff --git a/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts b/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts index a565e0e78dc..1583dcca363 100644 --- a/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts +++ b/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts @@ -48,7 +48,8 @@ export function phaseBindingSpecialization(job: CompilationJob): void { ir.OpList.replace( op, ir.createAttributeOp( - op.target, op.name, op.expression, op.isTemplate, op.sourceSpan)); + op.target, op.name, op.expression, op.securityContext, op.isTemplate, + op.sourceSpan)); } break; case ir.BindingKind.Property: @@ -62,7 +63,7 @@ export function phaseBindingSpecialization(job: CompilationJob): void { op, ir.createPropertyOp( op.target, op.name, op.expression, op.bindingKind === ir.BindingKind.Animation, - op.isTemplate, op.sourceSpan)); + op.securityContext, op.isTemplate, op.sourceSpan)); } break; diff --git a/packages/compiler/src/template/pipeline/src/phases/reify.ts b/packages/compiler/src/template/pipeline/src/phases/reify.ts index bfa7560e1f1..f857c492606 100644 --- a/packages/compiler/src/template/pipeline/src/phases/reify.ts +++ b/packages/compiler/src/template/pipeline/src/phases/reify.ts @@ -7,10 +7,22 @@ */ import * as o from '../../../../output/output_ast'; +import {Identifiers} from '../../../../render3/r3_identifiers'; import * as ir from '../../ir'; -import {type CompilationJob, type CompilationUnit, ViewCompilationUnit} from '../compilation'; +import {ViewCompilationUnit, type CompilationJob, type CompilationUnit} from '../compilation'; import * as ng from '../instruction'; +/** + * Map of sanitizers to their identifier. + */ +const sanitizerIdentifierMap = new Map([ + [ir.SanitizerFn.Html, Identifiers.sanitizeHtml], + [ir.SanitizerFn.IframeAttribute, Identifiers.validateIframeAttribute], + [ir.SanitizerFn.ResourceUrl, Identifiers.sanitizeResourceUrl], + [ir.SanitizerFn.Script, Identifiers.sanitizeScript], + [ir.SanitizerFn.Style, Identifiers.sanitizeStyle], [ir.SanitizerFn.Url, Identifiers.sanitizeUrl] +]); + /** * Compiles semantic operations across all views and generates output `o.Statement`s with actual * runtime calls in their place. @@ -150,9 +162,10 @@ function reifyUpdateOperations(_unit: CompilationUnit, ops: ir.OpList([ + [SecurityContext.HTML, ir.SanitizerFn.Html], [SecurityContext.SCRIPT, ir.SanitizerFn.Script], + [SecurityContext.STYLE, ir.SanitizerFn.Style], [SecurityContext.URL, ir.SanitizerFn.Url], + [SecurityContext.RESOURCE_URL, ir.SanitizerFn.ResourceUrl] +]); + +/** + * Resolves sanitization functions for ops that need them. + */ +export function phaseResolveSanitizers(cpl: ComponentCompilationJob): void { + for (const [_, view] of cpl.views) { + const elements = getElementsByXrefId(view); + let sanitizerFn: ir.SanitizerFn|null; + for (const op of view.update) { + switch (op.kind) { + case ir.OpKind.Property: + case ir.OpKind.Attribute: + sanitizerFn = sanitizers.get(op.securityContext) || null; + op.sanitizer = sanitizerFn ? new ir.SanitizerExpr(sanitizerFn) : null; + // If there was no sanitization function found based on the security context of an + // attribute/property, check whether this attribute/property is one of the + // security-sensitive