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:
Martin Probst
2016-04-29 16:04:08 -07:00
parent dd6e0cf1b5
commit 908a102a87
24 changed files with 590 additions and 34 deletions

View 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();
});
});
});
});
}