refactor(compiler): add support for sanitizing properties and attributes (#51156)

Sets sanitizer functions when attempting to set sensitive properties and attributes

PR Close #51156
This commit is contained in:
Miles Malerba 2023-07-28 10:17:26 -07:00 committed by Alex Rickabaugh
parent e4ae634230
commit c1052cf7a7
12 changed files with 352 additions and 120 deletions

View file

@ -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"]
}
]
}
]
}
}

View file

@ -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,
}

View file

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

View file

@ -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<UpdateOp> {
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<UpdateOp> {
*/
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<UpdateOp>, 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<UpdateOp>, 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<UpdateOp> {
*/
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<UpdateOp> {
* 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,

View file

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

View file

@ -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<e.BindingType, ir.BindingKind>([
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));
}
/**

View file

@ -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<OpT extends ir.CreateOp|ir.UpdateOp>(
}
/**
* 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[];

View file

@ -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<ir.XrefId, ir.ElementOrContainerOps>();
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<typeof lookupElement>;
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<ir.XrefId, ir.ElementOrContainerOps>) {
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);
}
}
}

View file

@ -48,7 +48,8 @@ export function phaseBindingSpecialization(job: CompilationJob): void {
ir.OpList.replace<ir.UpdateOp>(
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;

View file

@ -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, o.ExternalReference>([
[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<ir.UpdateO
ir.OpList.replace(
op,
ng.propertyInterpolate(
op.name, op.expression.strings, op.expression.expressions, op.sourceSpan));
op.name, op.expression.strings, op.expression.expressions, op.sanitizer,
op.sourceSpan));
} else {
ir.OpList.replace(op, ng.property(op.name, op.expression, op.sourceSpan));
ir.OpList.replace(op, ng.property(op.name, op.expression, op.sanitizer, op.sourceSpan));
}
break;
case ir.OpKind.StyleProp:
@ -194,9 +207,10 @@ function reifyUpdateOperations(_unit: CompilationUnit, ops: ir.OpList<ir.UpdateO
if (op.expression instanceof ir.Interpolation) {
ir.OpList.replace(
op,
ng.attributeInterpolate(op.name, op.expression.strings, op.expression.expressions));
ng.attributeInterpolate(
op.name, op.expression.strings, op.expression.expressions, op.sanitizer));
} else {
ir.OpList.replace(op, ng.attribute(op.name, op.expression));
ir.OpList.replace(op, ng.attribute(op.name, op.expression, op.sanitizer));
}
break;
case ir.OpKind.HostProperty:
@ -272,6 +286,8 @@ function reifyIrExpression(expr: o.Expression): o.Expression {
return ng.pipeBind(expr.slot!, expr.varOffset!, expr.args);
case ir.ExpressionKind.PipeBindingVariadic:
return ng.pipeBindV(expr.slot!, expr.varOffset!, expr.args);
case ir.ExpressionKind.SanitizerExpr:
return o.importExpr(sanitizerIdentifierMap.get(expr.fn)!);
default:
throw new Error(`AssertionError: Unsupported reification of ir.Expression kind: ${
ir.ExpressionKind[(expr as ir.Expression).kind]}`);

View file

@ -0,0 +1,62 @@
/**
* @license
* Copyright Google LLC 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
*/
import {SecurityContext} from '../../../../core';
import {isIframeSecuritySensitiveAttr} from '../../../../schema/dom_security_schema';
import * as ir from '../../ir';
import {ComponentCompilationJob} from '../compilation';
import {getElementsByXrefId} from '../util/elements';
/**
* Mapping of security contexts to sanitizer function for that context.
*/
const sanitizers = new Map<SecurityContext, ir.SanitizerFn|null>([
[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 <iframe> attributes (and that the current element is actually an
// <iframe>).
if (op.sanitizer === null) {
const ownerOp = elements.get(op.target);
if (ownerOp === undefined) {
throw Error('Property should have an element-like owner');
}
if (isIframeElement(ownerOp) && isIframeSecuritySensitiveAttr(op.name)) {
op.sanitizer = new ir.SanitizerExpr(ir.SanitizerFn.IframeAttribute);
}
}
break;
}
}
}
}
/**
* Checks whether the given op represents an iframe element.
*/
function isIframeElement(op: ir.ElementOrContainerOps): boolean {
return (op.kind === ir.OpKind.Element || op.kind === ir.OpKind.ElementStart) &&
op.tag.toLowerCase() === 'iframe';
}

View file

@ -0,0 +1,24 @@
/**
* @license
* Copyright Google LLC 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
*/
import * as ir from '../../ir';
import {ViewCompilationUnit} from '../compilation';
/**
* Gets a map of all elements in the given view by their xref id.
*/
export function getElementsByXrefId(view: ViewCompilationUnit) {
const elements = new Map<ir.XrefId, ir.ElementOrContainerOps>();
for (const op of view.create) {
if (!ir.isElementOrContainerOp(op)) {
continue;
}
elements.set(op.xref, op);
}
return elements;
}