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:
parent
dd6e0cf1b5
commit
908a102a87
@ -37,6 +37,10 @@ export var ValueUnwrapper: typeof t.ValueUnwrapper = r.ValueUnwrapper;
|
|||||||
export var TemplateRef_: typeof t.TemplateRef_ = r.TemplateRef_;
|
export var TemplateRef_: typeof t.TemplateRef_ = r.TemplateRef_;
|
||||||
export type RenderDebugInfo = t.RenderDebugInfo;
|
export type RenderDebugInfo = t.RenderDebugInfo;
|
||||||
export var RenderDebugInfo: typeof t.RenderDebugInfo = r.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 createProvider: typeof t.createProvider = r.createProvider;
|
||||||
export var isProviderLiteral: typeof t.isProviderLiteral = r.isProviderLiteral;
|
export var isProviderLiteral: typeof t.isProviderLiteral = r.isProviderLiteral;
|
||||||
export var EMPTY_ARRAY: typeof t.EMPTY_ARRAY = r.EMPTY_ARRAY;
|
export var EMPTY_ARRAY: typeof t.EMPTY_ARRAY = r.EMPTY_ARRAY;
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
TemplateRef
|
TemplateRef
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import {SecurityContext} from '../core_private';
|
||||||
import {
|
import {
|
||||||
AppElement,
|
AppElement,
|
||||||
AppView,
|
AppView,
|
||||||
@ -199,6 +200,11 @@ export class Identifiers {
|
|||||||
new CompileIdentifierMetadata(
|
new CompileIdentifierMetadata(
|
||||||
{name: 'pureProxy10', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy10}),
|
{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 {
|
export function identifierToken(identifier: CompileIdentifierMetadata): CompileTokenMetadata {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
|
import {SecurityContext} from '../../core_private';
|
||||||
import {isPresent} from '../facade/lang';
|
import {isPresent} from '../facade/lang';
|
||||||
import {StringMapWrapper} from '../facade/collection';
|
import {StringMapWrapper} from '../facade/collection';
|
||||||
import {ElementSchemaRegistry} from './element_schema_registry';
|
import {ElementSchemaRegistry} from './element_schema_registry';
|
||||||
@ -207,10 +208,11 @@ var attrToPropMap: {[name: string]: string} = <any>{
|
|||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DomElementSchemaRegistry implements ElementSchemaRegistry {
|
export class DomElementSchemaRegistry extends ElementSchemaRegistry {
|
||||||
schema = <{[element: string]: {[property: string]: string}}>{};
|
schema = <{[element: string]: {[property: string]: string}}>{};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
super();
|
||||||
SCHEMA.forEach(encodedType => {
|
SCHEMA.forEach(encodedType => {
|
||||||
var parts = encodedType.split('|');
|
var parts = encodedType.split('|');
|
||||||
var properties = parts[1].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 {
|
getMappedPropName(propName: string): string {
|
||||||
var mappedPropName = StringMapWrapper.get(attrToPropMap, propName);
|
var mappedPropName = StringMapWrapper.get(attrToPropMap, propName);
|
||||||
return isPresent(mappedPropName) ? mappedPropName : propName;
|
return isPresent(mappedPropName) ? mappedPropName : propName;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export class ElementSchemaRegistry {
|
export abstract class ElementSchemaRegistry {
|
||||||
hasProperty(tagName: string, propName: string): boolean { return true; }
|
abstract hasProperty(tagName: string, propName: string): boolean;
|
||||||
getMappedPropName(propName: string): string { return propName; }
|
abstract securityContext(tagName: string, propName: string): any;
|
||||||
|
abstract getMappedPropName(propName: string): string;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
CompileProviderMetadata,
|
CompileProviderMetadata,
|
||||||
} from './compile_metadata';
|
} from './compile_metadata';
|
||||||
import {ParseSourceSpan} from './parse_util';
|
import {ParseSourceSpan} from './parse_util';
|
||||||
|
import {SecurityContext} from '../core_private';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An Abstract Syntax Tree node representing part of a parsed Angular template.
|
* 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"`).
|
* A binding for an element property (e.g. `[property]="expression"`).
|
||||||
*/
|
*/
|
||||||
export class BoundElementPropertyAst implements TemplateAst {
|
export class BoundElementPropertyAst implements TemplateAst {
|
||||||
constructor(public name: string, public type: PropertyBindingType, public value: AST,
|
constructor(
|
||||||
public unit: string, public sourceSpan: ParseSourceSpan) {}
|
public name: string, public type: PropertyBindingType,
|
||||||
|
public securityContext: SecurityContext, public value: AST, public unit: string,
|
||||||
|
public sourceSpan: ParseSourceSpan) {}
|
||||||
visit(visitor: TemplateAstVisitor, context: any): any {
|
visit(visitor: TemplateAstVisitor, context: any): any {
|
||||||
return visitor.visitElementProperty(this, context);
|
return visitor.visitElementProperty(this, context);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {Injectable, Inject, OpaqueToken, Optional} from '@angular/core';
|
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 {
|
import {
|
||||||
ListWrapper,
|
ListWrapper,
|
||||||
@ -632,7 +632,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
|||||||
}
|
}
|
||||||
targetReferences.push(new ReferenceAst(elOrDirRef.name, refToken, elOrDirRef.sourceSpan));
|
targetReferences.push(new ReferenceAst(elOrDirRef.name, refToken, elOrDirRef.sourceSpan));
|
||||||
}
|
}
|
||||||
});
|
}); // fix syntax highlighting issue: `
|
||||||
return directiveAsts;
|
return directiveAsts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,10 +705,12 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
|||||||
sourceSpan: ParseSourceSpan): BoundElementPropertyAst {
|
sourceSpan: ParseSourceSpan): BoundElementPropertyAst {
|
||||||
var unit = null;
|
var unit = null;
|
||||||
var bindingType;
|
var bindingType;
|
||||||
var boundPropertyName;
|
var boundPropertyName: string;
|
||||||
var parts = name.split(PROPERTY_PARTS_SEPARATOR);
|
var parts = name.split(PROPERTY_PARTS_SEPARATOR);
|
||||||
|
let securityContext: SecurityContext;
|
||||||
if (parts.length === 1) {
|
if (parts.length === 1) {
|
||||||
boundPropertyName = this._schemaRegistry.getMappedPropName(parts[0]);
|
boundPropertyName = this._schemaRegistry.getMappedPropName(parts[0]);
|
||||||
|
securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName);
|
||||||
bindingType = PropertyBindingType.Property;
|
bindingType = PropertyBindingType.Property;
|
||||||
if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName)) {
|
if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName)) {
|
||||||
this._reportError(
|
this._reportError(
|
||||||
@ -718,27 +720,41 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
|||||||
} else {
|
} else {
|
||||||
if (parts[0] == ATTRIBUTE_PREFIX) {
|
if (parts[0] == ATTRIBUTE_PREFIX) {
|
||||||
boundPropertyName = parts[1];
|
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(':');
|
let nsSeparatorIdx = boundPropertyName.indexOf(':');
|
||||||
if (nsSeparatorIdx > -1) {
|
if (nsSeparatorIdx > -1) {
|
||||||
let ns = boundPropertyName.substring(0, nsSeparatorIdx);
|
let ns = boundPropertyName.substring(0, nsSeparatorIdx);
|
||||||
let name = boundPropertyName.substring(nsSeparatorIdx + 1);
|
let name = boundPropertyName.substring(nsSeparatorIdx + 1);
|
||||||
boundPropertyName = mergeNsAndName(ns, name);
|
boundPropertyName = mergeNsAndName(ns, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
bindingType = PropertyBindingType.Attribute;
|
bindingType = PropertyBindingType.Attribute;
|
||||||
} else if (parts[0] == CLASS_PREFIX) {
|
} else if (parts[0] == CLASS_PREFIX) {
|
||||||
boundPropertyName = parts[1];
|
boundPropertyName = parts[1];
|
||||||
bindingType = PropertyBindingType.Class;
|
bindingType = PropertyBindingType.Class;
|
||||||
|
securityContext = SecurityContext.NONE;
|
||||||
} else if (parts[0] == STYLE_PREFIX) {
|
} else if (parts[0] == STYLE_PREFIX) {
|
||||||
unit = parts.length > 2 ? parts[2] : null;
|
unit = parts.length > 2 ? parts[2] : null;
|
||||||
boundPropertyName = parts[1];
|
boundPropertyName = parts[1];
|
||||||
bindingType = PropertyBindingType.Style;
|
bindingType = PropertyBindingType.Style;
|
||||||
|
securityContext = SecurityContext.STYLE;
|
||||||
} else {
|
} else {
|
||||||
this._reportError(`Invalid property name '${name}'`, sourceSpan);
|
this._reportError(`Invalid property name '${name}'`, sourceSpan);
|
||||||
bindingType = null;
|
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 {LifecycleHooks, isDefaultChangeDetectionStrategy} from '../../core_private';
|
||||||
|
|
||||||
import {isBlank, isPresent} from '../../src/facade/lang';
|
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 cdAst from '../expression_parser/ast';
|
||||||
import * as o from '../output/output_ast';
|
import * as o from '../output/output_ast';
|
||||||
import {Identifiers} from '../identifiers';
|
import {Identifiers} from '../identifiers';
|
||||||
import {DetectChangesVars} from './constants';
|
import {DetectChangesVars, ViewProperties} from './constants';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BoundTextAst,
|
BoundTextAst,
|
||||||
@ -30,7 +31,7 @@ function createBindFieldExpr(exprIndex: number): o.ReadPropExpr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createCurrValueExpr(exprIndex: number): o.ReadVarExpr {
|
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,
|
function bind(view: CompileView, currValExpr: o.ReadVarExpr, fieldExpr: o.ReadPropExpr,
|
||||||
@ -94,7 +95,7 @@ function bindAndWriteToRenderer(boundProps: BoundElementPropertyAst[], context:
|
|||||||
var fieldExpr = createBindFieldExpr(bindingIndex);
|
var fieldExpr = createBindFieldExpr(bindingIndex);
|
||||||
var currValExpr = createCurrValueExpr(bindingIndex);
|
var currValExpr = createCurrValueExpr(bindingIndex);
|
||||||
var renderMethod: string;
|
var renderMethod: string;
|
||||||
var renderValue: o.Expression = currValExpr;
|
var renderValue: o.Expression = sanitizedValue(boundProp, currValExpr);
|
||||||
var updateStmts = [];
|
var updateStmts = [];
|
||||||
switch (boundProp.type) {
|
switch (boundProp.type) {
|
||||||
case PropertyBindingType.Property:
|
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[],
|
export function bindRenderInputs(boundProps: BoundElementPropertyAst[],
|
||||||
compileElement: CompileElement): void {
|
compileElement: CompileElement): void {
|
||||||
bindAndWriteToRenderer(boundProps, compileElement.view.componentContext, compileElement);
|
bindAndWriteToRenderer(boundProps, compileElement.view.componentContext, compileElement);
|
||||||
|
@ -214,7 +214,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
|
|||||||
var nestedComponentIdentifier =
|
var nestedComponentIdentifier =
|
||||||
new CompileIdentifierMetadata({name: getViewFactoryName(component, 0)});
|
new CompileIdentifierMetadata({name: getViewFactoryName(component, 0)});
|
||||||
this.targetDependencies.push(new ViewCompileDependency(component, nestedComponentIdentifier));
|
this.targetDependencies.push(new ViewCompileDependency(component, nestedComponentIdentifier));
|
||||||
compViewExpr = o.variable(`compView_${nodeIndex}`);
|
compViewExpr = o.variable(`compView_${nodeIndex}`); // fix highlighting: `
|
||||||
compileElement.setComponentView(compViewExpr);
|
compileElement.setComponentView(compViewExpr);
|
||||||
this.view.createMethod.addStmt(compViewExpr.set(o.importExpr(nestedComponentIdentifier)
|
this.view.createMethod.addStmt(compViewExpr.set(o.importExpr(nestedComponentIdentifier)
|
||||||
.callFn([
|
.callFn([
|
||||||
@ -336,7 +336,8 @@ function mapToKeyValueArray(data: {[key: string]: string}): string[][] {
|
|||||||
function createViewTopLevelStmts(view: CompileView, targetStatements: o.Statement[]) {
|
function createViewTopLevelStmts(view: CompileView, targetStatements: o.Statement[]) {
|
||||||
var nodeDebugInfosVar: o.Expression = o.NULL_EXPR;
|
var nodeDebugInfosVar: o.Expression = o.NULL_EXPR;
|
||||||
if (view.genConfig.genDebugInfo) {
|
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(
|
targetStatements.push(
|
||||||
(<o.ReadVarExpr>nodeDebugInfosVar)
|
(<o.ReadVarExpr>nodeDebugInfosVar)
|
||||||
.set(o.literalArr(view.nodes.map(createStaticNodeDebugInfo),
|
.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) {
|
if (view.viewIndex === 0) {
|
||||||
targetStatements.push(renderCompTypeVar.set(o.NULL_EXPR)
|
targetStatements.push(renderCompTypeVar.set(o.NULL_EXPR)
|
||||||
.toDeclStmt(o.importType(Identifiers.RenderComponentType)));
|
.toDeclStmt(o.importType(Identifiers.RenderComponentType)));
|
||||||
|
@ -11,11 +11,12 @@ import {
|
|||||||
} from '@angular/core/testing/testing_internal';
|
} from '@angular/core/testing/testing_internal';
|
||||||
|
|
||||||
import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry';
|
import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry';
|
||||||
|
import {SecurityContext} from '../../core_private';
|
||||||
import {extractSchema} from './schema_extractor';
|
import {extractSchema} from './schema_extractor';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('DOMElementSchema', () => {
|
describe('DOMElementSchema', () => {
|
||||||
var registry: DomElementSchemaRegistry;
|
let registry: DomElementSchemaRegistry;
|
||||||
beforeEach(() => { registry = new DomElementSchemaRegistry(); });
|
beforeEach(() => { registry = new DomElementSchemaRegistry(); });
|
||||||
|
|
||||||
it('should detect properties on regular elements', () => {
|
it('should detect properties on regular elements', () => {
|
||||||
@ -33,8 +34,7 @@ export function main() {
|
|||||||
expect(registry.hasProperty('div', 'unknown')).toBeFalsy();
|
expect(registry.hasProperty('div', 'unknown')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect different kinds of types',
|
it('should detect different kinds of types', () => {
|
||||||
() => {
|
|
||||||
// inheritance: video => media => *
|
// inheritance: video => media => *
|
||||||
expect(registry.hasProperty('video', 'className')).toBeTruthy(); // from *
|
expect(registry.hasProperty('video', 'className')).toBeTruthy(); // from *
|
||||||
expect(registry.hasProperty('video', 'id')).toBeTruthy(); // string
|
expect(registry.hasProperty('video', 'id')).toBeTruthy(); // string
|
||||||
@ -44,7 +44,7 @@ export function main() {
|
|||||||
expect(registry.hasProperty('video', 'classList')).toBeTruthy(); // object
|
expect(registry.hasProperty('video', 'classList')).toBeTruthy(); // object
|
||||||
// from *; but events are not properties
|
// from *; but events are not properties
|
||||||
expect(registry.hasProperty('video', 'click')).toBeFalsy();
|
expect(registry.hasProperty('video', 'click')).toBeFalsy();
|
||||||
})
|
});
|
||||||
|
|
||||||
it('should return true for custom-like elements',
|
it('should return true for custom-like elements',
|
||||||
() => { expect(registry.hasProperty('custom-like', 'unknown')).toBeTruthy(); });
|
() => { expect(registry.hasProperty('custom-like', 'unknown')).toBeTruthy(); });
|
||||||
@ -57,6 +57,10 @@ export function main() {
|
|||||||
expect(registry.getMappedPropName('exotic-unknown')).toEqual('exotic-unknown');
|
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',
|
it('should detect properties on namespaced elements',
|
||||||
() => { expect(registry.hasProperty('@svg:g', 'id')).toBeTruthy(); });
|
() => { expect(registry.hasProperty('@svg:g', 'id')).toBeTruthy(); });
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {isPresent} from '../src/facade/lang';
|
import {isPresent} from '../src/facade/lang';
|
||||||
|
import {SecurityContext} from '../core_private';
|
||||||
import {ElementSchemaRegistry} from '../index';
|
import {ElementSchemaRegistry} from '../index';
|
||||||
|
|
||||||
export class MockSchemaRegistry implements ElementSchemaRegistry {
|
export class MockSchemaRegistry implements ElementSchemaRegistry {
|
||||||
@ -10,6 +11,10 @@ export class MockSchemaRegistry implements ElementSchemaRegistry {
|
|||||||
return isPresent(result) ? result : true;
|
return isPresent(result) ? result : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityContext(tagName: string, property: string): SecurityContext {
|
||||||
|
return SecurityContext.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
getMappedPropName(attrName: string): string {
|
getMappedPropName(attrName: string): string {
|
||||||
var result = this.attrPropMapping[attrName];
|
var result = this.attrPropMapping[attrName];
|
||||||
return isPresent(result) ? result : attrName;
|
return isPresent(result) ? result : attrName;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import * as constants from './src/change_detection/constants';
|
import * as constants from './src/change_detection/constants';
|
||||||
|
import * as security from './src/security';
|
||||||
import * as reflective_provider from './src/di/reflective_provider';
|
import * as reflective_provider from './src/di/reflective_provider';
|
||||||
import * as lifecycle_hooks from './src/metadata/lifecycle_hooks';
|
import * as lifecycle_hooks from './src/metadata/lifecycle_hooks';
|
||||||
import * as reflector_reader from './src/reflection/reflector_reader';
|
import * as reflector_reader from './src/reflection/reflector_reader';
|
||||||
@ -52,6 +53,10 @@ export declare namespace __core_private_types__ {
|
|||||||
export var ValueUnwrapper: typeof change_detection_util.ValueUnwrapper;
|
export var ValueUnwrapper: typeof change_detection_util.ValueUnwrapper;
|
||||||
export type RenderDebugInfo = api.RenderDebugInfo;
|
export type RenderDebugInfo = api.RenderDebugInfo;
|
||||||
export var RenderDebugInfo: typeof api.RenderDebugInfo;
|
export var RenderDebugInfo: typeof api.RenderDebugInfo;
|
||||||
|
export var SecurityContext: typeof security.SecurityContext;
|
||||||
|
export type SecurityContext = security.SecurityContext;
|
||||||
|
export var SanitizationService: typeof security.SanitizationService;
|
||||||
|
export type SanitizationService = security.SanitizationService;
|
||||||
export type TemplateRef_<C> = template_ref.TemplateRef_<C>;
|
export type TemplateRef_<C> = template_ref.TemplateRef_<C>;
|
||||||
export var TemplateRef_: typeof template_ref.TemplateRef_;
|
export var TemplateRef_: typeof template_ref.TemplateRef_;
|
||||||
export var wtfInit: typeof wtf_init.wtfInit;
|
export var wtfInit: typeof wtf_init.wtfInit;
|
||||||
@ -104,6 +109,8 @@ export var __core_private__ = {
|
|||||||
uninitialized: change_detection_util.uninitialized,
|
uninitialized: change_detection_util.uninitialized,
|
||||||
ValueUnwrapper: change_detection_util.ValueUnwrapper,
|
ValueUnwrapper: change_detection_util.ValueUnwrapper,
|
||||||
RenderDebugInfo: api.RenderDebugInfo,
|
RenderDebugInfo: api.RenderDebugInfo,
|
||||||
|
SecurityContext: security.SecurityContext,
|
||||||
|
SanitizationService: security.SanitizationService,
|
||||||
TemplateRef_: template_ref.TemplateRef_,
|
TemplateRef_: template_ref.TemplateRef_,
|
||||||
wtfInit: wtf_init.wtfInit,
|
wtfInit: wtf_init.wtfInit,
|
||||||
ReflectionCapabilities: reflection_capabilities.ReflectionCapabilities,
|
ReflectionCapabilities: reflection_capabilities.ReflectionCapabilities,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import {SanitizationService} from '../security';
|
||||||
import {isBlank, isPresent, looseIdentical} from '../../src/facade/lang';
|
import {isBlank, isPresent, looseIdentical} from '../../src/facade/lang';
|
||||||
import {ListWrapper, StringMapWrapper} from '../../src/facade/collection';
|
import {ListWrapper, StringMapWrapper} from '../../src/facade/collection';
|
||||||
import {BaseException} from '../../src/facade/exceptions';
|
import {BaseException} from '../../src/facade/exceptions';
|
||||||
@ -12,9 +13,14 @@ import {uninitialized} from "../change_detection/change_detection_util";
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ViewUtils {
|
export class ViewUtils {
|
||||||
|
sanitizer: SanitizationService;
|
||||||
private _nextCompTypeId: number = 0;
|
private _nextCompTypeId: number = 0;
|
||||||
|
|
||||||
constructor(private _renderer: RootRenderer, @Inject(APP_ID) private _appId: string) {}
|
constructor(
|
||||||
|
private _renderer: RootRenderer, @Inject(APP_ID) private _appId: string,
|
||||||
|
sanitizer: SanitizationService) {
|
||||||
|
this.sanitizer = sanitizer;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by the generated code
|
* Used by the generated code
|
||||||
|
23
modules/@angular/core/src/security.ts
Normal file
23
modules/@angular/core/src/security.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* A SecurityContext marks a location that has dangerous security implications, e.g. a DOM property
|
||||||
|
* like `innerHTML` that could cause Cross Site Scripting (XSS) security bugs when improperly
|
||||||
|
* handled.
|
||||||
|
*
|
||||||
|
* See DomSanitizationService for more details on security in Angular applications.
|
||||||
|
*/
|
||||||
|
export enum SecurityContext {
|
||||||
|
NONE,
|
||||||
|
HTML,
|
||||||
|
STYLE,
|
||||||
|
SCRIPT,
|
||||||
|
URL,
|
||||||
|
RESOURCE_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SanitizationService is used by the views to sanitize potentially dangerous values. This is a
|
||||||
|
* private API, use code should only refer to DomSanitizationService.
|
||||||
|
*/
|
||||||
|
export abstract class SanitizationService {
|
||||||
|
abstract sanitize(context: SecurityContext, value: string): string;
|
||||||
|
}
|
157
modules/@angular/core/test/linker/security_integration_spec.ts
Normal file
157
modules/@angular/core/test/linker/security_integration_spec.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import {
|
||||||
|
ddescribe,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
inject,
|
||||||
|
beforeEachProviders,
|
||||||
|
it,
|
||||||
|
} from '@angular/core/testing/testing_internal';
|
||||||
|
import {TestComponentBuilder} from '@angular/compiler/testing';
|
||||||
|
import {AsyncTestCompleter} from '@angular/core/testing/testing_internal';
|
||||||
|
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||||
|
import {PromiseWrapper} from '../../src/facade/async';
|
||||||
|
import {provide, Injectable, OpaqueToken} from '@angular/core';
|
||||||
|
import {CompilerConfig} from '@angular/compiler';
|
||||||
|
import {Component, ViewMetadata} from '@angular/core/src/metadata';
|
||||||
|
import {IS_DART} from '../../src/facade/lang';
|
||||||
|
import {el} from '@angular/platform-browser/testing';
|
||||||
|
|
||||||
|
import {DomSanitizationService} from '@angular/platform-browser';
|
||||||
|
|
||||||
|
const ANCHOR_ELEMENT = /*@ts2dart_const*/ new OpaqueToken('AnchorElement');
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
if (IS_DART) {
|
||||||
|
declareTests(false);
|
||||||
|
} else {
|
||||||
|
describe('jit', () => {
|
||||||
|
beforeEachProviders(
|
||||||
|
() => [provide(CompilerConfig, {useValue: new CompilerConfig(true, false, true)})]);
|
||||||
|
declareTests(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('no jit', () => {
|
||||||
|
beforeEachProviders(
|
||||||
|
() => [provide(CompilerConfig, {useValue: new CompilerConfig(true, false, false)})]);
|
||||||
|
declareTests(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'my-comp', directives: []})
|
||||||
|
@Injectable()
|
||||||
|
class SecuredComponent {
|
||||||
|
ctxProp: string;
|
||||||
|
constructor() { this.ctxProp = 'some value'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function itAsync(msg: string, injections: Function[], f: Function);
|
||||||
|
function itAsync(msg: string, f: (tcb: TestComponentBuilder, atc: AsyncTestCompleter) => void);
|
||||||
|
function itAsync(msg: string,
|
||||||
|
f: Function[] | ((tcb: TestComponentBuilder, atc: AsyncTestCompleter) => void),
|
||||||
|
fn?: Function) {
|
||||||
|
if (f instanceof Function) {
|
||||||
|
it(msg, inject([TestComponentBuilder, AsyncTestCompleter], <Function>f));
|
||||||
|
} else {
|
||||||
|
let injections = f;
|
||||||
|
it(msg, inject(injections, fn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function declareTests(isJit: boolean) {
|
||||||
|
describe('security integration tests', function() {
|
||||||
|
|
||||||
|
beforeEachProviders(() => [provide(ANCHOR_ELEMENT, {useValue: el('<div></div>')})]);
|
||||||
|
|
||||||
|
describe('safe HTML values', function() {
|
||||||
|
itAsync('should disallow binding on*', (tcb: TestComponentBuilder, async) => {
|
||||||
|
let tpl = `<div [attr.onclick]="ctxProp"></div>`;
|
||||||
|
tcb = tcb.overrideView(SecuredComponent, new ViewMetadata({template: tpl}));
|
||||||
|
PromiseWrapper.catchError(tcb.createAsync(SecuredComponent), (e) => {
|
||||||
|
expect(e.message).toContain(
|
||||||
|
`Template parse errors:\n` + `Binding to event attribute 'onclick' is disallowed ` +
|
||||||
|
`for security reasons, please use (click)=... `);
|
||||||
|
async.done();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
itAsync('should escape unsafe attributes', (tcb: TestComponentBuilder, async) => {
|
||||||
|
let tpl = `<a [href]="ctxProp">Link Title</a>`;
|
||||||
|
tcb.overrideView(SecuredComponent, new ViewMetadata({template: tpl, directives: []}))
|
||||||
|
.createAsync(SecuredComponent)
|
||||||
|
.then((fixture) => {
|
||||||
|
let e = fixture.debugElement.children[0].nativeElement;
|
||||||
|
fixture.debugElement.componentInstance.ctxProp = 'hello';
|
||||||
|
fixture.detectChanges();
|
||||||
|
// In the browser, reading href returns an absolute URL. On the server side,
|
||||||
|
// it just echoes back the property.
|
||||||
|
expect(getDOM().getProperty(e, 'href')).toMatch(/.*\/?hello$/);
|
||||||
|
|
||||||
|
fixture.debugElement.componentInstance.ctxProp = 'javascript:alert(1)';
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getDOM().getProperty(e, 'href')).toEqual('unsafe:javascript:alert(1)');
|
||||||
|
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
itAsync('should not escape values marked as trusted',
|
||||||
|
[TestComponentBuilder, AsyncTestCompleter, DomSanitizationService],
|
||||||
|
(tcb: TestComponentBuilder, async, sanitizer: DomSanitizationService) => {
|
||||||
|
let tpl = `<a [href]="ctxProp">Link Title</a>`;
|
||||||
|
tcb.overrideView(SecuredComponent,
|
||||||
|
new ViewMetadata({template: tpl, directives: []}))
|
||||||
|
.createAsync(SecuredComponent)
|
||||||
|
.then((fixture) => {
|
||||||
|
let e = fixture.debugElement.children[0].nativeElement;
|
||||||
|
let trusted = sanitizer.bypassSecurityTrustUrl('javascript:alert(1)');
|
||||||
|
fixture.debugElement.componentInstance.ctxProp = trusted;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getDOM().getProperty(e, 'href')).toEqual('javascript:alert(1)');
|
||||||
|
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
itAsync('should error when using the wrong trusted value',
|
||||||
|
[TestComponentBuilder, AsyncTestCompleter, DomSanitizationService],
|
||||||
|
(tcb: TestComponentBuilder, async, sanitizer: DomSanitizationService) => {
|
||||||
|
let tpl = `<a [href]="ctxProp">Link Title</a>`;
|
||||||
|
tcb.overrideView(SecuredComponent,
|
||||||
|
new ViewMetadata({template: tpl, directives: []}))
|
||||||
|
.createAsync(SecuredComponent)
|
||||||
|
.then((fixture) => {
|
||||||
|
let trusted = sanitizer.bypassSecurityTrustScript('javascript:alert(1)');
|
||||||
|
fixture.debugElement.componentInstance.ctxProp = trusted;
|
||||||
|
expect(() => fixture.detectChanges())
|
||||||
|
.toThrowErrorWith('Required a safe URL, got a Script');
|
||||||
|
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
itAsync('should escape unsafe style values', (tcb: TestComponentBuilder, async) => {
|
||||||
|
let tpl = `<div [style.background]="ctxProp">Text</div>`;
|
||||||
|
tcb.overrideView(SecuredComponent, new ViewMetadata({template: tpl, directives: []}))
|
||||||
|
.createAsync(SecuredComponent)
|
||||||
|
.then((fixture) => {
|
||||||
|
let e = fixture.debugElement.children[0].nativeElement;
|
||||||
|
// Make sure binding harmless values works.
|
||||||
|
fixture.debugElement.componentInstance.ctxProp = 'red';
|
||||||
|
fixture.detectChanges();
|
||||||
|
// In some browsers, this will contain the full background specification, not just
|
||||||
|
// the color.
|
||||||
|
expect(getDOM().getStyle(e, 'background')).toMatch(/red.*/);
|
||||||
|
|
||||||
|
fixture.debugElement.componentInstance.ctxProp = 'url(javascript:evil())';
|
||||||
|
fixture.detectChanges();
|
||||||
|
// Updated value gets rejected, no value change.
|
||||||
|
expect(getDOM().getStyle(e, 'background')).not.toContain('javascript');
|
||||||
|
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -362,6 +362,7 @@ var PLATFORM_BROWSER: string[] = [
|
|||||||
'BROWSER_APP_STATIC_PROVIDERS',
|
'BROWSER_APP_STATIC_PROVIDERS',
|
||||||
'BROWSER_PROVIDERS',
|
'BROWSER_PROVIDERS',
|
||||||
'BROWSER_APP_COMMON_PROVIDERS',
|
'BROWSER_APP_COMMON_PROVIDERS',
|
||||||
|
'BROWSER_SANITIZATION_PROVIDERS',
|
||||||
'DOCUMENT',
|
'DOCUMENT',
|
||||||
'ELEMENT_PROBE_PROVIDERS',
|
'ELEMENT_PROBE_PROVIDERS',
|
||||||
'DomEventsPlugin',
|
'DomEventsPlugin',
|
||||||
@ -375,6 +376,8 @@ var PLATFORM_BROWSER: string[] = [
|
|||||||
'BrowserPlatformLocation',
|
'BrowserPlatformLocation',
|
||||||
'AngularEntrypoint:dart',
|
'AngularEntrypoint:dart',
|
||||||
'By',
|
'By',
|
||||||
|
'DomSanitizationService',
|
||||||
|
'SecurityContext',
|
||||||
'Title',
|
'Title',
|
||||||
'disableDebugTools',
|
'disableDebugTools',
|
||||||
'enableDebugTools'
|
'enableDebugTools'
|
||||||
|
@ -8,3 +8,7 @@ export var VIEW_ENCAPSULATION_VALUES: typeof t.VIEW_ENCAPSULATION_VALUES =
|
|||||||
r.VIEW_ENCAPSULATION_VALUES;
|
r.VIEW_ENCAPSULATION_VALUES;
|
||||||
export type DebugDomRootRenderer = t.DebugDomRootRenderer;
|
export type DebugDomRootRenderer = t.DebugDomRootRenderer;
|
||||||
export var DebugDomRootRenderer: typeof t.DebugDomRootRenderer = r.DebugDomRootRenderer;
|
export var DebugDomRootRenderer: typeof t.DebugDomRootRenderer = r.DebugDomRootRenderer;
|
||||||
|
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;
|
||||||
|
@ -10,8 +10,12 @@ import {
|
|||||||
OpaqueToken,
|
OpaqueToken,
|
||||||
Testability
|
Testability
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {wtfInit} from '../core_private';
|
import {wtfInit, SanitizationService} from '../core_private';
|
||||||
import {COMMON_DIRECTIVES, COMMON_PIPES, FORM_PROVIDERS} from '@angular/common';
|
import {COMMON_DIRECTIVES, COMMON_PIPES, FORM_PROVIDERS} from '@angular/common';
|
||||||
|
import {
|
||||||
|
DomSanitizationService,
|
||||||
|
DomSanitizationServiceImpl
|
||||||
|
} from './security/dom_sanitization_service';
|
||||||
|
|
||||||
import {IS_DART} from './facade/lang';
|
import {IS_DART} from './facade/lang';
|
||||||
import {BrowserDomAdapter} from './browser/browser_adapter';
|
import {BrowserDomAdapter} from './browser/browser_adapter';
|
||||||
@ -62,6 +66,11 @@ function _document(): any {
|
|||||||
return getDOM().defaultDoc();
|
return getDOM().defaultDoc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const BROWSER_SANITIZATION_PROVIDERS: Array<any> = /*@ts2dart_const*/[
|
||||||
|
/* @ts2dart_Provider */ {provide: SanitizationService, useExisting: DomSanitizationService},
|
||||||
|
/* @ts2dart_Provider */ {provide: DomSanitizationService, useClass: DomSanitizationServiceImpl},
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A set of providers to initialize an Angular application in a web browser.
|
* A set of providers to initialize an Angular application in a web browser.
|
||||||
*
|
*
|
||||||
@ -71,6 +80,7 @@ export const BROWSER_APP_COMMON_PROVIDERS: Array<any /*Type | Provider | any[]*/
|
|||||||
/*@ts2dart_const*/[
|
/*@ts2dart_const*/[
|
||||||
APPLICATION_COMMON_PROVIDERS,
|
APPLICATION_COMMON_PROVIDERS,
|
||||||
FORM_PROVIDERS,
|
FORM_PROVIDERS,
|
||||||
|
BROWSER_SANITIZATION_PROVIDERS,
|
||||||
/* @ts2dart_Provider */ {provide: PLATFORM_PIPES, useValue: COMMON_PIPES, multi: true},
|
/* @ts2dart_Provider */ {provide: PLATFORM_PIPES, useValue: COMMON_PIPES, multi: true},
|
||||||
/* @ts2dart_Provider */ {provide: PLATFORM_DIRECTIVES, useValue: COMMON_DIRECTIVES, multi: true},
|
/* @ts2dart_Provider */ {provide: PLATFORM_DIRECTIVES, useValue: COMMON_DIRECTIVES, multi: true},
|
||||||
/* @ts2dart_Provider */ {provide: ExceptionHandler, useFactory: _exceptionHandler, deps: []},
|
/* @ts2dart_Provider */ {provide: ExceptionHandler, useFactory: _exceptionHandler, deps: []},
|
||||||
|
@ -13,6 +13,7 @@ export {EventManager, EVENT_MANAGER_PLUGINS} from './dom/events/event_manager';
|
|||||||
export {ELEMENT_PROBE_PROVIDERS} from './dom/debug/ng_probe';
|
export {ELEMENT_PROBE_PROVIDERS} from './dom/debug/ng_probe';
|
||||||
export {
|
export {
|
||||||
BROWSER_APP_COMMON_PROVIDERS,
|
BROWSER_APP_COMMON_PROVIDERS,
|
||||||
|
BROWSER_SANITIZATION_PROVIDERS,
|
||||||
BROWSER_PROVIDERS,
|
BROWSER_PROVIDERS,
|
||||||
By,
|
By,
|
||||||
Title,
|
Title,
|
||||||
@ -25,6 +26,7 @@ export {
|
|||||||
export * from '../private_export';
|
export * from '../private_export';
|
||||||
export {DOCUMENT} from './dom/dom_tokens';
|
export {DOCUMENT} from './dom/dom_tokens';
|
||||||
|
|
||||||
|
export {DomSanitizationService, SecurityContext} from './security/dom_sanitization_service';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
bootstrapStatic,
|
bootstrapStatic,
|
||||||
|
@ -0,0 +1,171 @@
|
|||||||
|
import {sanitizeUrl} from './url_sanitizer';
|
||||||
|
import {sanitizeStyle} from './style_sanitizer';
|
||||||
|
import {SecurityContext, SanitizationService} from '../../core_private';
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
export {SecurityContext};
|
||||||
|
|
||||||
|
/** Marker interface for a value that's safe to use in a particular context. */
|
||||||
|
export interface SafeValue {}
|
||||||
|
/** Marker interface for a value that's safe to use as HTML. */
|
||||||
|
export interface SafeHtml extends SafeValue {}
|
||||||
|
/** Marker interface for a value that's safe to use as style (CSS). */
|
||||||
|
export interface SafeStyle extends SafeValue {}
|
||||||
|
/** Marker interface for a value that's safe to use as JavaScript. */
|
||||||
|
export interface SafeScript extends SafeValue {}
|
||||||
|
/** Marker interface for a value that's safe to use as a URL linking to a document. */
|
||||||
|
export interface SafeUrl extends SafeValue {}
|
||||||
|
/** Marker interface for a value that's safe to use as a URL to load executable code from. */
|
||||||
|
export interface SafeResourceUrl extends SafeValue {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DomSanitizationService helps preventing Cross Site Scripting Security bugs (XSS) by sanitizing
|
||||||
|
* values to be safe to use in the different DOM contexts.
|
||||||
|
*
|
||||||
|
* For example, when binding a URL in an `<a [href]="someValue">` hyperlink, `someValue` will be
|
||||||
|
* sanitized so that an attacker cannot inject e.g. a `javascript:` URL that would execute code on
|
||||||
|
* the website.
|
||||||
|
*
|
||||||
|
* In specific situations, it might be necessary to disable sanitization, for example if the
|
||||||
|
* application genuinely needs to produce a `javascript:` style link with a dynamic value in it.
|
||||||
|
* Users can bypass security by constructing a value with one of the `bypassSecurityTrust...`
|
||||||
|
* methods, and then binding to that value from the template.
|
||||||
|
*
|
||||||
|
* These situations should be very rare, and extraordinary care must be taken to avoid creating a
|
||||||
|
* Cross Site Scripting (XSS) security bug!
|
||||||
|
*
|
||||||
|
* When using `bypassSecurityTrust...`, make sure to call the method as early as possible and as
|
||||||
|
* close as possible to the source of the value, to make it easy to verify no security bug is
|
||||||
|
* created by its use.
|
||||||
|
*
|
||||||
|
* It is not required (and not recommended) to bypass security if the value is safe, e.g. a URL that
|
||||||
|
* does not start with a suspicious protocol, or an HTML snippet that does not contain dangerous
|
||||||
|
* code. The sanitizer leaves safe values intact.
|
||||||
|
*/
|
||||||
|
export abstract class DomSanitizationService implements SanitizationService {
|
||||||
|
/**
|
||||||
|
* Sanitizes a value for use in the given SecurityContext.
|
||||||
|
*
|
||||||
|
* If value is trusted for the context, this method will unwrap the contained safe value and use
|
||||||
|
* it directly. Otherwise, value will be sanitized to be safe in the given context, for example
|
||||||
|
* by replacing URLs that have an unsafe protocol part (such as `javascript:`). The implementation
|
||||||
|
* is responsible to make sure that the value can definitely be safely used in the given context.
|
||||||
|
*/
|
||||||
|
abstract sanitize(context: SecurityContext, value: any): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bypass security and trust the given value to be safe HTML. Only use this when the bound HTML
|
||||||
|
* is unsafe (e.g. contains `<script>` tags) and the code should be executed. The sanitizer will
|
||||||
|
* leave safe HTML intact, so in most situations this method should not be used.
|
||||||
|
*
|
||||||
|
* WARNING: calling this method with untrusted user data will cause severe security bugs!
|
||||||
|
*/
|
||||||
|
abstract bypassSecurityTrustHtml(value: string): SafeHtml;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bypass security and trust the given value to be safe style value (CSS).
|
||||||
|
*
|
||||||
|
* WARNING: calling this method with untrusted user data will cause severe security bugs!
|
||||||
|
*/
|
||||||
|
abstract bypassSecurityTrustStyle(value: string): SafeStyle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bypass security and trust the given value to be safe JavaScript.
|
||||||
|
*
|
||||||
|
* WARNING: calling this method with untrusted user data will cause severe security bugs!
|
||||||
|
*/
|
||||||
|
abstract bypassSecurityTrustScript(value: string): SafeScript;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bypass security and trust the given value to be a safe style URL, i.e. a value that can be used
|
||||||
|
* in hyperlinks or `<iframe src>`.
|
||||||
|
*
|
||||||
|
* WARNING: calling this method with untrusted user data will cause severe security bugs!
|
||||||
|
*/
|
||||||
|
abstract bypassSecurityTrustUrl(value: string): SafeUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bypass security and trust the given value to be a safe resource URL, i.e. a location that may
|
||||||
|
* be used to load executable code from, like `<script src>`.
|
||||||
|
*
|
||||||
|
* WARNING: calling this method with untrusted user data will cause severe security bugs!
|
||||||
|
*/
|
||||||
|
abstract bypassSecurityTrustResourceUrl(value: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DomSanitizationServiceImpl extends DomSanitizationService {
|
||||||
|
sanitize(ctx: SecurityContext, value: any): string {
|
||||||
|
if (value == null) return null;
|
||||||
|
switch (ctx) {
|
||||||
|
case SecurityContext.NONE:
|
||||||
|
return value;
|
||||||
|
case SecurityContext.HTML:
|
||||||
|
if (value instanceof SafeHtmlImpl) return value.changingThisBreaksApplicationSecurity;
|
||||||
|
this.checkNotSafeValue(value, 'HTML');
|
||||||
|
return this.sanitizeHtml(String(value));
|
||||||
|
case SecurityContext.STYLE:
|
||||||
|
if (value instanceof SafeStyleImpl) return value.changingThisBreaksApplicationSecurity;
|
||||||
|
this.checkNotSafeValue(value, 'Style');
|
||||||
|
return sanitizeStyle(value);
|
||||||
|
case SecurityContext.SCRIPT:
|
||||||
|
if (value instanceof SafeScriptImpl) return value.changingThisBreaksApplicationSecurity;
|
||||||
|
this.checkNotSafeValue(value, 'Script');
|
||||||
|
throw new Error('unsafe value used in a script context');
|
||||||
|
case SecurityContext.URL:
|
||||||
|
if (value instanceof SafeUrlImpl) return value.changingThisBreaksApplicationSecurity;
|
||||||
|
this.checkNotSafeValue(value, 'URL');
|
||||||
|
return sanitizeUrl(String(value));
|
||||||
|
case SecurityContext.RESOURCE_URL:
|
||||||
|
if (value instanceof SafeResourceUrlImpl) {
|
||||||
|
return value.changingThisBreaksApplicationSecurity;
|
||||||
|
}
|
||||||
|
this.checkNotSafeValue(value, 'ResourceURL');
|
||||||
|
throw new Error('unsafe value used in a resource URL context');
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected SecurityContext ${ctx}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkNotSafeValue(value: any, expectedType: string) {
|
||||||
|
if (value instanceof SafeValueImpl) {
|
||||||
|
throw new Error('Required a safe ' + expectedType + ', got a ' + value.getTypeName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizeHtml(value: string): string {
|
||||||
|
// TODO(martinprobst): implement.
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bypassSecurityTrustHtml(value: string): SafeHtml { return new SafeHtmlImpl(value); }
|
||||||
|
bypassSecurityTrustStyle(value: string): SafeStyle { return new SafeStyleImpl(value); }
|
||||||
|
bypassSecurityTrustScript(value: string): SafeScript { return new SafeScriptImpl(value); }
|
||||||
|
bypassSecurityTrustUrl(value: string): SafeUrl { return new SafeUrlImpl(value); }
|
||||||
|
bypassSecurityTrustResourceUrl(value: string): SafeResourceUrl {
|
||||||
|
return new SafeResourceUrlImpl(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class SafeValueImpl implements SafeValue {
|
||||||
|
constructor(public changingThisBreaksApplicationSecurity: string) {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
abstract getTypeName(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SafeHtmlImpl extends SafeValueImpl implements SafeHtml {
|
||||||
|
getTypeName() { return 'HTML'; }
|
||||||
|
}
|
||||||
|
class SafeStyleImpl extends SafeValueImpl implements SafeStyle {
|
||||||
|
getTypeName() { return 'Style'; }
|
||||||
|
}
|
||||||
|
class SafeScriptImpl extends SafeValueImpl implements SafeScript {
|
||||||
|
getTypeName() { return 'Script'; }
|
||||||
|
}
|
||||||
|
class SafeUrlImpl extends SafeValueImpl implements SafeUrl {
|
||||||
|
getTypeName() { return 'URL'; }
|
||||||
|
}
|
||||||
|
class SafeResourceUrlImpl extends SafeValueImpl implements SafeResourceUrl {
|
||||||
|
getTypeName() { return 'ResourceURL'; }
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Regular expression for safe style values.
|
||||||
|
*
|
||||||
|
* Quotes (" and ') are allowed, but a check must be done elsewhere to ensure
|
||||||
|
* they're balanced.
|
||||||
|
*
|
||||||
|
* ',' allows multiple values to be assigned to the same property
|
||||||
|
* (e.g. background-attachment or font-family) and hence could allow
|
||||||
|
* multiple values to get injected, but that should pose no risk of XSS.
|
||||||
|
*
|
||||||
|
* The rgb() and rgba() expression checks only for XSS safety, not for CSS
|
||||||
|
* validity.
|
||||||
|
*
|
||||||
|
* This regular expression was taken from the Closure sanitization library.
|
||||||
|
*/
|
||||||
|
const SAFE_STYLE_VALUE = /^([-,."'%_!# a-zA-Z0-9]+|(?:rgb|hsl)a?\([0-9.%, ]+\))$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that quotes (" and ') are properly balanced inside a string. Assumes
|
||||||
|
* that neither escape (\) nor any other character that could result in
|
||||||
|
* breaking out of a string parsing context are allowed;
|
||||||
|
* see http://www.w3.org/TR/css3-syntax/#string-token-diagram.
|
||||||
|
*
|
||||||
|
* This code was taken from the Closure sanitization library.
|
||||||
|
*/
|
||||||
|
function hasBalancedQuotes(value: string) {
|
||||||
|
let outsideSingle = true;
|
||||||
|
let outsideDouble = true;
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
let c = value.charAt(i);
|
||||||
|
if (c === '\'' && outsideDouble) {
|
||||||
|
outsideSingle = !outsideSingle;
|
||||||
|
} else if (c === '"' && outsideSingle) {
|
||||||
|
outsideDouble = !outsideDouble;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outsideSingle && outsideDouble;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeStyle(value: string): string {
|
||||||
|
if (String(value).match(SAFE_STYLE_VALUE) && hasBalancedQuotes(value)) return value;
|
||||||
|
return 'unsafe';
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* A pattern that recognizes a commonly useful subset of URLs that are safe.
|
||||||
|
*
|
||||||
|
* This regular expression matches a subset of URLs that will not cause script
|
||||||
|
* execution if used in URL context within a HTML document. Specifically, this
|
||||||
|
* regular expression matches if (comment from here on and regex copied from
|
||||||
|
* Soy's EscapingConventions):
|
||||||
|
* (1) Either a protocol in a whitelist (http, https, mailto or ftp).
|
||||||
|
* (2) or no protocol. A protocol must be followed by a colon. The below
|
||||||
|
* allows that by allowing colons only after one of the characters [/?#].
|
||||||
|
* A colon after a hash (#) must be in the fragment.
|
||||||
|
* Otherwise, a colon after a (?) must be in a query.
|
||||||
|
* Otherwise, a colon after a single solidus (/) must be in a path.
|
||||||
|
* Otherwise, a colon after a double solidus (//) must be in the authority
|
||||||
|
* (before port).
|
||||||
|
*
|
||||||
|
* The pattern disallows &, used in HTML entity declarations before
|
||||||
|
* one of the characters in [/?#]. This disallows HTML entities used in the
|
||||||
|
* protocol name, which should never happen, e.g. "http" for "http".
|
||||||
|
* It also disallows HTML entities in the first path part of a relative path,
|
||||||
|
* e.g. "foo<bar/baz". Our existing escaping functions should not produce
|
||||||
|
* that. More importantly, it disallows masking of a colon,
|
||||||
|
* e.g. "javascript:...".
|
||||||
|
*
|
||||||
|
* This regular expression was taken from the Closure sanitization library.
|
||||||
|
*/
|
||||||
|
const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi;
|
||||||
|
|
||||||
|
export function sanitizeUrl(url: string): string {
|
||||||
|
if (String(url).match(SAFE_URL_PATTERN)) return url;
|
||||||
|
return 'unsafe:' + url;
|
||||||
|
}
|
@ -24,6 +24,7 @@ import {AnimationBuilder} from '../animate/animation_builder';
|
|||||||
import {Testability} from '@angular/core/src/testability/testability';
|
import {Testability} from '@angular/core/src/testability/testability';
|
||||||
import {BrowserGetTestability} from '@angular/platform-browser/src/browser/testability';
|
import {BrowserGetTestability} from '@angular/platform-browser/src/browser/testability';
|
||||||
import {BrowserDomAdapter} from '../browser/browser_adapter';
|
import {BrowserDomAdapter} from '../browser/browser_adapter';
|
||||||
|
import {BROWSER_SANITIZATION_PROVIDERS} from '../browser_common';
|
||||||
import {wtfInit} from '@angular/core/src/profile/wtf_init';
|
import {wtfInit} from '@angular/core/src/profile/wtf_init';
|
||||||
import {MessageBasedRenderer} from '../web_workers/ui/renderer';
|
import {MessageBasedRenderer} from '../web_workers/ui/renderer';
|
||||||
import {
|
import {
|
||||||
@ -41,6 +42,8 @@ import {Serializer} from '../web_workers/shared/serializer';
|
|||||||
import {ON_WEB_WORKER} from '../web_workers/shared/api';
|
import {ON_WEB_WORKER} from '../web_workers/shared/api';
|
||||||
import {RenderStore} from '../web_workers/shared/render_store';
|
import {RenderStore} from '../web_workers/shared/render_store';
|
||||||
import {HAMMER_GESTURE_CONFIG, HammerGestureConfig} from '../dom/events/hammer_gestures';
|
import {HAMMER_GESTURE_CONFIG, HammerGestureConfig} from '../dom/events/hammer_gestures';
|
||||||
|
import {SanitizationService} from '../../core_private';
|
||||||
|
import {DomSanitizationService} from '../security/dom_sanitization_service';
|
||||||
import {EventManager, EVENT_MANAGER_PLUGINS} from '../dom/events/event_manager';
|
import {EventManager, EVENT_MANAGER_PLUGINS} from '../dom/events/event_manager';
|
||||||
import {XHR} from "../../../compiler/src/xhr";
|
import {XHR} from "../../../compiler/src/xhr";
|
||||||
import {XHRImpl} from "../../../platform-browser-dynamic/src/xhr/xhr_impl";
|
import {XHRImpl} from "../../../platform-browser-dynamic/src/xhr/xhr_impl";
|
||||||
@ -73,6 +76,7 @@ export const WORKER_RENDER_APPLICATION_COMMON: Array<any /*Type | Provider | any
|
|||||||
/*@ts2dart_const*/[
|
/*@ts2dart_const*/[
|
||||||
APPLICATION_COMMON_PROVIDERS,
|
APPLICATION_COMMON_PROVIDERS,
|
||||||
WORKER_RENDER_MESSAGING_PROVIDERS,
|
WORKER_RENDER_MESSAGING_PROVIDERS,
|
||||||
|
BROWSER_SANITIZATION_PROVIDERS,
|
||||||
/* @ts2dart_Provider */ {provide: ExceptionHandler, useFactory: _exceptionHandler, deps: []},
|
/* @ts2dart_Provider */ {provide: ExceptionHandler, useFactory: _exceptionHandler, deps: []},
|
||||||
/* @ts2dart_Provider */ {provide: DOCUMENT, useFactory: _document, deps: []},
|
/* @ts2dart_Provider */ {provide: DOCUMENT, useFactory: _document, deps: []},
|
||||||
// TODO(jteplitz602): Investigate if we definitely need EVENT_MANAGER on the render thread
|
// TODO(jteplitz602): Investigate if we definitely need EVENT_MANAGER on the render thread
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
PLATFORM_COMMON_PROVIDERS,
|
PLATFORM_COMMON_PROVIDERS,
|
||||||
PLATFORM_INITIALIZER,
|
PLATFORM_INITIALIZER,
|
||||||
APPLICATION_COMMON_PROVIDERS,
|
APPLICATION_COMMON_PROVIDERS,
|
||||||
Renderer
|
Renderer,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {DirectiveResolver, ViewResolver} from '@angular/compiler';
|
import {DirectiveResolver, ViewResolver} from '@angular/compiler';
|
||||||
import {TestComponentBuilder} from '@angular/compiler/testing';
|
import {TestComponentBuilder} from '@angular/compiler/testing';
|
||||||
@ -20,6 +20,7 @@ import {BrowserDetection} from '@angular/platform-browser/testing';
|
|||||||
|
|
||||||
import {COMPILER_PROVIDERS} from '@angular/compiler';
|
import {COMPILER_PROVIDERS} from '@angular/compiler';
|
||||||
import {DOCUMENT} from '@angular/platform-browser';
|
import {DOCUMENT} from '@angular/platform-browser';
|
||||||
|
import {BROWSER_SANITIZATION_PROVIDERS} from '@angular/platform-browser';
|
||||||
import {getDOM} from '../platform_browser_private';
|
import {getDOM} from '../platform_browser_private';
|
||||||
import {RootRenderer} from '@angular/core';
|
import {RootRenderer} from '@angular/core';
|
||||||
import {DomRootRenderer, DomRootRenderer_} from '../../platform-browser/src/dom/dom_renderer';
|
import {DomRootRenderer, DomRootRenderer_} from '../../platform-browser/src/dom/dom_renderer';
|
||||||
@ -75,6 +76,7 @@ export const TEST_SERVER_APPLICATION_PROVIDERS: Array<any /*Type | Provider | an
|
|||||||
// list here.
|
// list here.
|
||||||
APPLICATION_COMMON_PROVIDERS,
|
APPLICATION_COMMON_PROVIDERS,
|
||||||
COMPILER_PROVIDERS,
|
COMPILER_PROVIDERS,
|
||||||
|
BROWSER_SANITIZATION_PROVIDERS,
|
||||||
/* @ts2dart_Provider */ {provide: DOCUMENT, useFactory: appDoc},
|
/* @ts2dart_Provider */ {provide: DOCUMENT, useFactory: appDoc},
|
||||||
/* @ts2dart_Provider */ {provide: DomRootRenderer, useClass: DomRootRenderer_},
|
/* @ts2dart_Provider */ {provide: DomRootRenderer, useClass: DomRootRenderer_},
|
||||||
/* @ts2dart_Provider */ {provide: RootRenderer, useExisting: DomRootRenderer},
|
/* @ts2dart_Provider */ {provide: RootRenderer, useExisting: DomRootRenderer},
|
||||||
|
@ -1223,6 +1223,8 @@ const BROWSER = [
|
|||||||
'By.all():Predicate<DebugElement>',
|
'By.all():Predicate<DebugElement>',
|
||||||
'By.css(selector:string):Predicate<DebugElement>',
|
'By.css(selector:string):Predicate<DebugElement>',
|
||||||
'By.directive(type:Type):Predicate<DebugElement>',
|
'By.directive(type:Type):Predicate<DebugElement>',
|
||||||
|
'DomSanitizationService',
|
||||||
|
'SecurityContext',
|
||||||
'Title',
|
'Title',
|
||||||
'Title.getTitle():string',
|
'Title.getTitle():string',
|
||||||
'Title.setTitle(newTitle:string):any',
|
'Title.setTitle(newTitle:string):any',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user