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:
@ -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;
|
||||
}
|
Reference in New Issue
Block a user