mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
e4ae634230
commit
c1052cf7a7
12 changed files with 352 additions and 120 deletions
|
|
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]}`);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
24
packages/compiler/src/template/pipeline/src/util/elements.ts
Normal file
24
packages/compiler/src/template/pipeline/src/util/elements.ts
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue