feat: security implementation in Angular 2.
Summary: This adds basic security hooks to Angular 2. * `SecurityContext` is a private API between core, compiler, and platform-browser. `SecurityContext` communicates what context a value is used in across template parser, compiler, and sanitization at runtime. * `SanitizationService` is the bare bones interface to sanitize values for a particular context. * `SchemaElementRegistry.securityContext(tagName, attributeOrPropertyName)` determines the security context for an attribute or property (it turns out attributes and properties match for the purposes of sanitization). Based on these hooks: * `DomSchemaElementRegistry` decides what sanitization applies in a particular context. * `DomSanitizationService` implements `SanitizationService` and adds *Safe Value*s, i.e. the ability to mark a value as safe and not requiring further sanitization. * `url_sanitizer` and `style_sanitizer` sanitize URLs and Styles, respectively (surprise!). `DomSanitizationService` is the default implementation bound for browser applications, in the three contexts (browser rendering, web worker rendering, server side rendering). BREAKING CHANGES: *** SECURITY WARNING *** Angular 2 Release Candidates do not implement proper contextual escaping yet. Make sure to correctly escape all values that go into the DOM. *** SECURITY WARNING *** Reviewers: IgorMinar Differential Revision: https://reviews.angular.io/D103
This commit is contained in:
@ -37,6 +37,10 @@ export var ValueUnwrapper: typeof t.ValueUnwrapper = r.ValueUnwrapper;
|
||||
export var TemplateRef_: typeof t.TemplateRef_ = r.TemplateRef_;
|
||||
export type RenderDebugInfo = t.RenderDebugInfo;
|
||||
export var RenderDebugInfo: typeof t.RenderDebugInfo = r.RenderDebugInfo;
|
||||
export var SecurityContext: typeof t.SecurityContext = r.SecurityContext;
|
||||
export type SecurityContext = t.SecurityContext;
|
||||
export var SanitizationService: typeof t.SanitizationService = r.SanitizationService;
|
||||
export type SanitizationService = t.SanitizationService;
|
||||
export var createProvider: typeof t.createProvider = r.createProvider;
|
||||
export var isProviderLiteral: typeof t.isProviderLiteral = r.isProviderLiteral;
|
||||
export var EMPTY_ARRAY: typeof t.EMPTY_ARRAY = r.EMPTY_ARRAY;
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
ViewEncapsulation,
|
||||
TemplateRef
|
||||
} from '@angular/core';
|
||||
import {SecurityContext} from '../core_private';
|
||||
import {
|
||||
AppElement,
|
||||
AppView,
|
||||
@ -199,6 +200,11 @@ export class Identifiers {
|
||||
new CompileIdentifierMetadata(
|
||||
{name: 'pureProxy10', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy10}),
|
||||
];
|
||||
static SecurityContext = new CompileIdentifierMetadata({
|
||||
name: 'SecurityContext',
|
||||
moduleUrl: assetUrl('core', 'security'),
|
||||
runtime: SecurityContext,
|
||||
});
|
||||
}
|
||||
|
||||
export function identifierToken(identifier: CompileIdentifierMetadata): CompileTokenMetadata {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {SecurityContext} from '../../core_private';
|
||||
import {isPresent} from '../facade/lang';
|
||||
import {StringMapWrapper} from '../facade/collection';
|
||||
import {ElementSchemaRegistry} from './element_schema_registry';
|
||||
@ -207,10 +208,11 @@ var attrToPropMap: {[name: string]: string} = <any>{
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class DomElementSchemaRegistry implements ElementSchemaRegistry {
|
||||
export class DomElementSchemaRegistry extends ElementSchemaRegistry {
|
||||
schema = <{[element: string]: {[property: string]: string}}>{};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
SCHEMA.forEach(encodedType => {
|
||||
var parts = encodedType.split('|');
|
||||
var properties = parts[1].split(',');
|
||||
@ -254,6 +256,24 @@ export class DomElementSchemaRegistry implements ElementSchemaRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* securityContext returns the security context for the given property on the given DOM tag.
|
||||
*
|
||||
* Tag and property name are statically known and cannot change at runtime, i.e. it is not
|
||||
* possible to bind a value into a changing attribute or tag name.
|
||||
*
|
||||
* The filtering is white list based. All attributes in the schema above are assumed to have the
|
||||
* 'NONE' security context, i.e. that they are safe inert string values. Only specific well known
|
||||
* attack vectors are assigned their appropriate context.
|
||||
*/
|
||||
securityContext(tagName: string, propName: string): SecurityContext {
|
||||
// TODO(martinprobst): Fill in missing properties.
|
||||
if (propName === 'style') return SecurityContext.STYLE;
|
||||
if (tagName === 'a' && propName === 'href') return SecurityContext.URL;
|
||||
if (propName === 'innerHTML') return SecurityContext.HTML;
|
||||
return SecurityContext.NONE;
|
||||
}
|
||||
|
||||
getMappedPropName(propName: string): string {
|
||||
var mappedPropName = StringMapWrapper.get(attrToPropMap, propName);
|
||||
return isPresent(mappedPropName) ? mappedPropName : propName;
|
||||
|
@ -1,4 +1,5 @@
|
||||
export class ElementSchemaRegistry {
|
||||
hasProperty(tagName: string, propName: string): boolean { return true; }
|
||||
getMappedPropName(propName: string): string { return propName; }
|
||||
export abstract class ElementSchemaRegistry {
|
||||
abstract hasProperty(tagName: string, propName: string): boolean;
|
||||
abstract securityContext(tagName: string, propName: string): any;
|
||||
abstract getMappedPropName(propName: string): string;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
CompileProviderMetadata,
|
||||
} from './compile_metadata';
|
||||
import {ParseSourceSpan} from './parse_util';
|
||||
import {SecurityContext} from '../core_private';
|
||||
|
||||
/**
|
||||
* An Abstract Syntax Tree node representing part of a parsed Angular template.
|
||||
@ -54,8 +55,10 @@ export class AttrAst implements TemplateAst {
|
||||
* A binding for an element property (e.g. `[property]="expression"`).
|
||||
*/
|
||||
export class BoundElementPropertyAst implements TemplateAst {
|
||||
constructor(public name: string, public type: PropertyBindingType, public value: AST,
|
||||
public unit: string, public sourceSpan: ParseSourceSpan) {}
|
||||
constructor(
|
||||
public name: string, public type: PropertyBindingType,
|
||||
public securityContext: SecurityContext, public value: AST, public unit: string,
|
||||
public sourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: TemplateAstVisitor, context: any): any {
|
||||
return visitor.visitElementProperty(this, context);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Injectable, Inject, OpaqueToken, Optional} from '@angular/core';
|
||||
import {MAX_INTERPOLATION_VALUES, Console} from '../core_private';
|
||||
import {MAX_INTERPOLATION_VALUES, Console, SecurityContext} from '../core_private';
|
||||
|
||||
import {
|
||||
ListWrapper,
|
||||
@ -632,7 +632,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
||||
}
|
||||
targetReferences.push(new ReferenceAst(elOrDirRef.name, refToken, elOrDirRef.sourceSpan));
|
||||
}
|
||||
});
|
||||
}); // fix syntax highlighting issue: `
|
||||
return directiveAsts;
|
||||
}
|
||||
|
||||
@ -705,10 +705,12 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
||||
sourceSpan: ParseSourceSpan): BoundElementPropertyAst {
|
||||
var unit = null;
|
||||
var bindingType;
|
||||
var boundPropertyName;
|
||||
var boundPropertyName: string;
|
||||
var parts = name.split(PROPERTY_PARTS_SEPARATOR);
|
||||
let securityContext: SecurityContext;
|
||||
if (parts.length === 1) {
|
||||
boundPropertyName = this._schemaRegistry.getMappedPropName(parts[0]);
|
||||
securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName);
|
||||
bindingType = PropertyBindingType.Property;
|
||||
if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName)) {
|
||||
this._reportError(
|
||||
@ -718,27 +720,41 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
||||
} else {
|
||||
if (parts[0] == ATTRIBUTE_PREFIX) {
|
||||
boundPropertyName = parts[1];
|
||||
if (boundPropertyName.toLowerCase().startsWith('on')) {
|
||||
this._reportError(
|
||||
`Binding to event attribute '${boundPropertyName}' is disallowed ` +
|
||||
`for security reasons, please use (${boundPropertyName.slice(2)})=...`,
|
||||
sourceSpan);
|
||||
}
|
||||
// NB: For security purposes, use the mapped property name, not the attribute name.
|
||||
securityContext = this._schemaRegistry.securityContext(
|
||||
elementName, this._schemaRegistry.getMappedPropName(boundPropertyName));
|
||||
let nsSeparatorIdx = boundPropertyName.indexOf(':');
|
||||
if (nsSeparatorIdx > -1) {
|
||||
let ns = boundPropertyName.substring(0, nsSeparatorIdx);
|
||||
let name = boundPropertyName.substring(nsSeparatorIdx + 1);
|
||||
boundPropertyName = mergeNsAndName(ns, name);
|
||||
}
|
||||
|
||||
bindingType = PropertyBindingType.Attribute;
|
||||
} else if (parts[0] == CLASS_PREFIX) {
|
||||
boundPropertyName = parts[1];
|
||||
bindingType = PropertyBindingType.Class;
|
||||
securityContext = SecurityContext.NONE;
|
||||
} else if (parts[0] == STYLE_PREFIX) {
|
||||
unit = parts.length > 2 ? parts[2] : null;
|
||||
boundPropertyName = parts[1];
|
||||
bindingType = PropertyBindingType.Style;
|
||||
securityContext = SecurityContext.STYLE;
|
||||
} else {
|
||||
this._reportError(`Invalid property name '${name}'`, sourceSpan);
|
||||
bindingType = null;
|
||||
securityContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
return new BoundElementPropertyAst(boundPropertyName, bindingType, ast, unit, sourceSpan);
|
||||
return new BoundElementPropertyAst(boundPropertyName, bindingType, securityContext, ast, unit,
|
||||
sourceSpan);
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import {SecurityContext} from '../../core_private';
|
||||
import {LifecycleHooks, isDefaultChangeDetectionStrategy} from '../../core_private';
|
||||
|
||||
import {isBlank, isPresent} from '../../src/facade/lang';
|
||||
@ -5,7 +6,7 @@ import {isBlank, isPresent} from '../../src/facade/lang';
|
||||
import * as cdAst from '../expression_parser/ast';
|
||||
import * as o from '../output/output_ast';
|
||||
import {Identifiers} from '../identifiers';
|
||||
import {DetectChangesVars} from './constants';
|
||||
import {DetectChangesVars, ViewProperties} from './constants';
|
||||
|
||||
import {
|
||||
BoundTextAst,
|
||||
@ -30,7 +31,7 @@ function createBindFieldExpr(exprIndex: number): o.ReadPropExpr {
|
||||
}
|
||||
|
||||
function createCurrValueExpr(exprIndex: number): o.ReadVarExpr {
|
||||
return o.variable(`currVal_${exprIndex}`);
|
||||
return o.variable(`currVal_${exprIndex}`); // fix syntax highlighting: `
|
||||
}
|
||||
|
||||
function bind(view: CompileView, currValExpr: o.ReadVarExpr, fieldExpr: o.ReadPropExpr,
|
||||
@ -94,7 +95,7 @@ function bindAndWriteToRenderer(boundProps: BoundElementPropertyAst[], context:
|
||||
var fieldExpr = createBindFieldExpr(bindingIndex);
|
||||
var currValExpr = createCurrValueExpr(bindingIndex);
|
||||
var renderMethod: string;
|
||||
var renderValue: o.Expression = currValExpr;
|
||||
var renderValue: o.Expression = sanitizedValue(boundProp, currValExpr);
|
||||
var updateStmts = [];
|
||||
switch (boundProp.type) {
|
||||
case PropertyBindingType.Property:
|
||||
@ -130,6 +131,34 @@ function bindAndWriteToRenderer(boundProps: BoundElementPropertyAst[], context:
|
||||
});
|
||||
}
|
||||
|
||||
function sanitizedValue(boundProp: BoundElementPropertyAst, renderValue: o.Expression): o.Expression {
|
||||
let enumValue: string;
|
||||
switch (boundProp.securityContext) {
|
||||
case SecurityContext.NONE:
|
||||
return renderValue; // No sanitization needed.
|
||||
case SecurityContext.HTML:
|
||||
enumValue = 'HTML';
|
||||
break;
|
||||
case SecurityContext.STYLE:
|
||||
enumValue = 'STYLE';
|
||||
break;
|
||||
case SecurityContext.SCRIPT:
|
||||
enumValue = 'SCRIPT';
|
||||
break;
|
||||
case SecurityContext.URL:
|
||||
enumValue = 'URL';
|
||||
break;
|
||||
case SecurityContext.RESOURCE_URL:
|
||||
enumValue = 'RESOURCE_URL';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`internal error, unexpected SecurityContext ${boundProp.securityContext}.`);
|
||||
}
|
||||
let ctx = ViewProperties.viewUtils.prop('sanitizer');
|
||||
let args = [o.importExpr(Identifiers.SecurityContext).prop(enumValue), renderValue];
|
||||
return ctx.callMethod('sanitize', args);
|
||||
}
|
||||
|
||||
export function bindRenderInputs(boundProps: BoundElementPropertyAst[],
|
||||
compileElement: CompileElement): void {
|
||||
bindAndWriteToRenderer(boundProps, compileElement.view.componentContext, compileElement);
|
||||
|
@ -214,7 +214,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
|
||||
var nestedComponentIdentifier =
|
||||
new CompileIdentifierMetadata({name: getViewFactoryName(component, 0)});
|
||||
this.targetDependencies.push(new ViewCompileDependency(component, nestedComponentIdentifier));
|
||||
compViewExpr = o.variable(`compView_${nodeIndex}`);
|
||||
compViewExpr = o.variable(`compView_${nodeIndex}`); // fix highlighting: `
|
||||
compileElement.setComponentView(compViewExpr);
|
||||
this.view.createMethod.addStmt(compViewExpr.set(o.importExpr(nestedComponentIdentifier)
|
||||
.callFn([
|
||||
@ -336,7 +336,8 @@ function mapToKeyValueArray(data: {[key: string]: string}): string[][] {
|
||||
function createViewTopLevelStmts(view: CompileView, targetStatements: o.Statement[]) {
|
||||
var nodeDebugInfosVar: o.Expression = o.NULL_EXPR;
|
||||
if (view.genConfig.genDebugInfo) {
|
||||
nodeDebugInfosVar = o.variable(`nodeDebugInfos_${view.component.type.name}${view.viewIndex}`);
|
||||
nodeDebugInfosVar = o.variable(
|
||||
`nodeDebugInfos_${view.component.type.name}${view.viewIndex}`); // fix highlighting: `
|
||||
targetStatements.push(
|
||||
(<o.ReadVarExpr>nodeDebugInfosVar)
|
||||
.set(o.literalArr(view.nodes.map(createStaticNodeDebugInfo),
|
||||
@ -346,7 +347,8 @@ function createViewTopLevelStmts(view: CompileView, targetStatements: o.Statemen
|
||||
}
|
||||
|
||||
|
||||
var renderCompTypeVar: o.ReadVarExpr = o.variable(`renderType_${view.component.type.name}`);
|
||||
var renderCompTypeVar: o.ReadVarExpr =
|
||||
o.variable(`renderType_${view.component.type.name}`); // fix highlighting: `
|
||||
if (view.viewIndex === 0) {
|
||||
targetStatements.push(renderCompTypeVar.set(o.NULL_EXPR)
|
||||
.toDeclStmt(o.importType(Identifiers.RenderComponentType)));
|
||||
|
@ -11,11 +11,12 @@ import {
|
||||
} from '@angular/core/testing/testing_internal';
|
||||
|
||||
import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry';
|
||||
import {SecurityContext} from '../../core_private';
|
||||
import {extractSchema} from './schema_extractor';
|
||||
|
||||
export function main() {
|
||||
describe('DOMElementSchema', () => {
|
||||
var registry: DomElementSchemaRegistry;
|
||||
let registry: DomElementSchemaRegistry;
|
||||
beforeEach(() => { registry = new DomElementSchemaRegistry(); });
|
||||
|
||||
it('should detect properties on regular elements', () => {
|
||||
@ -33,21 +34,20 @@ export function main() {
|
||||
expect(registry.hasProperty('div', 'unknown')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should detect different kinds of types',
|
||||
() => {
|
||||
// inheritance: video => media => *
|
||||
expect(registry.hasProperty('video', 'className')).toBeTruthy(); // from *
|
||||
expect(registry.hasProperty('video', 'id')).toBeTruthy(); // string
|
||||
expect(registry.hasProperty('video', 'scrollLeft')).toBeTruthy(); // number
|
||||
expect(registry.hasProperty('video', 'height')).toBeTruthy(); // number
|
||||
expect(registry.hasProperty('video', 'autoplay')).toBeTruthy(); // boolean
|
||||
expect(registry.hasProperty('video', 'classList')).toBeTruthy(); // object
|
||||
// from *; but events are not properties
|
||||
expect(registry.hasProperty('video', 'click')).toBeFalsy();
|
||||
})
|
||||
it('should detect different kinds of types', () => {
|
||||
// inheritance: video => media => *
|
||||
expect(registry.hasProperty('video', 'className')).toBeTruthy(); // from *
|
||||
expect(registry.hasProperty('video', 'id')).toBeTruthy(); // string
|
||||
expect(registry.hasProperty('video', 'scrollLeft')).toBeTruthy(); // number
|
||||
expect(registry.hasProperty('video', 'height')).toBeTruthy(); // number
|
||||
expect(registry.hasProperty('video', 'autoplay')).toBeTruthy(); // boolean
|
||||
expect(registry.hasProperty('video', 'classList')).toBeTruthy(); // object
|
||||
// from *; but events are not properties
|
||||
expect(registry.hasProperty('video', 'click')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return true for custom-like elements',
|
||||
() => { expect(registry.hasProperty('custom-like', 'unknown')).toBeTruthy(); });
|
||||
it('should return true for custom-like elements',
|
||||
() => { expect(registry.hasProperty('custom-like', 'unknown')).toBeTruthy(); });
|
||||
|
||||
it('should re-map property names that are specified in DOM facade',
|
||||
() => { expect(registry.getMappedPropName('readonly')).toEqual('readOnly'); });
|
||||
@ -57,6 +57,10 @@ export function main() {
|
||||
expect(registry.getMappedPropName('exotic-unknown')).toEqual('exotic-unknown');
|
||||
});
|
||||
|
||||
it('should return security contexts for elements', () => {
|
||||
expect(registry.securityContext('a', 'href')).toBe(SecurityContext.URL);
|
||||
});
|
||||
|
||||
it('should detect properties on namespaced elements',
|
||||
() => { expect(registry.hasProperty('@svg:g', 'id')).toBeTruthy(); });
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {isPresent} from '../src/facade/lang';
|
||||
import {SecurityContext} from '../core_private';
|
||||
import {ElementSchemaRegistry} from '../index';
|
||||
|
||||
export class MockSchemaRegistry implements ElementSchemaRegistry {
|
||||
@ -10,6 +11,10 @@ export class MockSchemaRegistry implements ElementSchemaRegistry {
|
||||
return isPresent(result) ? result : true;
|
||||
}
|
||||
|
||||
securityContext(tagName: string, property: string): SecurityContext {
|
||||
return SecurityContext.NONE;
|
||||
}
|
||||
|
||||
getMappedPropName(attrName: string): string {
|
||||
var result = this.attrPropMapping[attrName];
|
||||
return isPresent(result) ? result : attrName;
|
||||
|
Reference in New Issue
Block a user