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:
@ -10,8 +10,12 @@ import {
|
||||
OpaqueToken,
|
||||
Testability
|
||||
} 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 {
|
||||
DomSanitizationService,
|
||||
DomSanitizationServiceImpl
|
||||
} from './security/dom_sanitization_service';
|
||||
|
||||
import {IS_DART} from './facade/lang';
|
||||
import {BrowserDomAdapter} from './browser/browser_adapter';
|
||||
@ -62,6 +66,11 @@ function _document(): any {
|
||||
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.
|
||||
*
|
||||
@ -71,6 +80,7 @@ export const BROWSER_APP_COMMON_PROVIDERS: Array<any /*Type | Provider | any[]*/
|
||||
/*@ts2dart_const*/[
|
||||
APPLICATION_COMMON_PROVIDERS,
|
||||
FORM_PROVIDERS,
|
||||
BROWSER_SANITIZATION_PROVIDERS,
|
||||
/* @ts2dart_Provider */ {provide: PLATFORM_PIPES, useValue: COMMON_PIPES, multi: true},
|
||||
/* @ts2dart_Provider */ {provide: PLATFORM_DIRECTIVES, useValue: COMMON_DIRECTIVES, multi: true},
|
||||
/* @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 {
|
||||
BROWSER_APP_COMMON_PROVIDERS,
|
||||
BROWSER_SANITIZATION_PROVIDERS,
|
||||
BROWSER_PROVIDERS,
|
||||
By,
|
||||
Title,
|
||||
@ -25,6 +26,7 @@ export {
|
||||
export * from '../private_export';
|
||||
export {DOCUMENT} from './dom/dom_tokens';
|
||||
|
||||
export {DomSanitizationService, SecurityContext} from './security/dom_sanitization_service';
|
||||
|
||||
export {
|
||||
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 {BrowserGetTestability} from '@angular/platform-browser/src/browser/testability';
|
||||
import {BrowserDomAdapter} from '../browser/browser_adapter';
|
||||
import {BROWSER_SANITIZATION_PROVIDERS} from '../browser_common';
|
||||
import {wtfInit} from '@angular/core/src/profile/wtf_init';
|
||||
import {MessageBasedRenderer} from '../web_workers/ui/renderer';
|
||||
import {
|
||||
@ -41,6 +42,8 @@ import {Serializer} from '../web_workers/shared/serializer';
|
||||
import {ON_WEB_WORKER} from '../web_workers/shared/api';
|
||||
import {RenderStore} from '../web_workers/shared/render_store';
|
||||
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 {XHR} from "../../../compiler/src/xhr";
|
||||
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*/[
|
||||
APPLICATION_COMMON_PROVIDERS,
|
||||
WORKER_RENDER_MESSAGING_PROVIDERS,
|
||||
BROWSER_SANITIZATION_PROVIDERS,
|
||||
/* @ts2dart_Provider */ {provide: ExceptionHandler, useFactory: _exceptionHandler, deps: []},
|
||||
/* @ts2dart_Provider */ {provide: DOCUMENT, useFactory: _document, deps: []},
|
||||
// TODO(jteplitz602): Investigate if we definitely need EVENT_MANAGER on the render thread
|
||||
|
Reference in New Issue
Block a user