Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
fix(ngCsp): allow CSP to be configurable
Browse files Browse the repository at this point in the history
There are two different features in Angular that can break CSP rules:
use of `eval` to execute a string as JavaScript and dynamic injection of
CSS style rules into the DOM.

This change allows us to configure which of these features should be turned
off to allow a more fine grained set of CSP rules to be supported.

Closes #11933
Closes #8459
Closes #12346
  • Loading branch information
petebacondarwin committed Jul 16, 2015
1 parent 1f4aa47 commit 618356e
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 50 deletions.
2 changes: 1 addition & 1 deletion lib/grunt/utils.js
Expand Up @@ -116,7 +116,7 @@ module.exports = {
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\r?\n/g, '\\n');
js = "!window.angular.$$csp() && window.angular.element(document.head).prepend('<style type=\"text/css\">" + css + "</style>');";
js = "!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend('<style type=\"text/css\">" + css + "</style>');";
state.js.push(js);

return state;
Expand Down
31 changes: 24 additions & 7 deletions src/Angular.js
Expand Up @@ -984,22 +984,39 @@ function equals(o1, o2) {
}

var csp = function() {
if (isDefined(csp.isActive_)) return csp.isActive_;
if (!isDefined(csp.rules)) {

var active = !!(document.querySelector('[ng-csp]') ||
document.querySelector('[data-ng-csp]'));

if (!active) {
var ngCspElement = (document.querySelector('[ng-csp]') ||
document.querySelector('[data-ng-csp]'));

if (ngCspElement) {
var ngCspAttribute = ngCspElement.getAttribute('ng-csp') ||
ngCspElement.getAttribute('data-ng-csp');
csp.rules = {
noUnsafeEval: !ngCspAttribute || (ngCspAttribute.indexOf('no-unsafe-eval') !== -1),
noInlineStyle: !ngCspAttribute || (ngCspAttribute.indexOf('no-inline-style') !== -1)
};
} else {
csp.rules = {
noUnsafeEval: noUnsafeEval(),
noInlineStyle: false
};
}
}

return csp.rules;

function noUnsafeEval() {
try {
/* jshint -W031, -W054 */
new Function('');
/* jshint +W031, +W054 */
return false;
} catch (e) {
active = true;
return true;
}
}

return (csp.isActive_ = active);
};

/**
Expand Down
1 change: 0 additions & 1 deletion src/AngularPublic.js
Expand Up @@ -20,7 +20,6 @@
ngClassDirective,
ngClassEvenDirective,
ngClassOddDirective,
ngCspDirective,
ngCloakDirective,
ngControllerDirective,
ngFormDirective,
Expand Down
64 changes: 47 additions & 17 deletions src/ng/directive/ngCsp.js
Expand Up @@ -6,27 +6,29 @@
*
* @element html
* @description
* Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support.
*
* Angular has some features that can break certain
* [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules.
*
* If you intend to implement these rules then you must tell Angular not to use these features.
*
* This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps.
*
* CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things).
* For Angular to be CSP compatible there are only two things that we need to do differently:
*
* - don't use `Function` constructor to generate optimized value getters
* - don't inject custom stylesheet into the document
* The following rules affect Angular:
*
* AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp`
* directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will
* evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will
* be raised.
* * `unsafe-eval`: this rule forbids apps to use `eval` or `Function(string)` generated functions
* (among other things). Angular makes use of this in the {@link $parse} service to provide a 30%
* increase in the speed of evaluating Angular expressions.
*
* CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically
* includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}).
* To make those directives work in CSP mode, include the `angular-csp.css` manually.
* * `unsafe-inline`: this rule forbids apps from inject custom styles into the document. Angular
* makes use of this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}).
* To make these directives work when a CSP rule is blocking inline styles, you must link to the
* `angular-csp.css` in your HTML manually.
*
* Angular tries to autodetect if CSP is active and automatically turn on the CSP-safe mode. This
* autodetection however triggers a CSP error to be logged in the console:
* If you do not provide `ngCsp` then Angular tries to autodetect if CSP is blocking unsafe-eval
* and automatically deactivates this feature in the {@link $parse} service. This autodetection,
* however, triggers a CSP error to be logged in the console:
*
* ```
* Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of
Expand All @@ -35,11 +37,39 @@
* ```
*
* This error is harmless but annoying. To prevent the error from showing up, put the `ngCsp`
* directive on the root element of the application or on the `angular.js` script tag, whichever
* appears first in the html document.
* directive on an element of the HTML document that appears before the `<script>` tag that loads
* the `angular.js` file.
*
* *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.*
*
* You can specify which of the CSP related Angular features should be deactivated by providing
* a value for the `ng-csp` attribute. The options are as follows:
*
* * no-inline-style: this stops Angular from injecting CSS styles into the DOM
*
* * no-unsafe-eval: this stops Angular from optimising $parse with unsafe eval of strings
*
* You can use these values in the following combinations:
*
*
* * No declaration means that Angular will assume that you can do inline styles, but it will do
* a runtime check for unsafe-eval. E.g. `<body>`. This is backwardly compatible with previous versions
* of Angular.
*
* * A simple `ng-csp` (or `data-ng-csp`) attribute will tell Angular to deactivate both inline
* styles and unsafe eval. E.g. `<body ng-csp>`. This is backwardly compatible with previous versions
* of Angular.
*
* * Specifying only `no-unsafe-eval` tells Angular that we must not use eval, but that we can inject
* inline styles. E.g. `<body ng-csp="no-unsafe-eval">`.
*
* * Specifying only `no-inline-style` tells Angular that we must not inject styles, but that we can
* run eval - no automcatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">`
*
* * Specifying both `no-unsafe-eval` and `no-inline-style` tells Angular that we must not inject
* styles nor use eval, which is the same as an empty: ng-csp.
* E.g.`<body ng-csp="no-inline-style;no-unsafe-eval">`
*
* @example
* This example shows how to apply the `ngCsp` directive to the `html` tag.
```html
Expand Down Expand Up @@ -171,4 +201,4 @@

// ngCsp is not implemented as a proper directive any more, because we need it be processed while we
// bootstrap the system (before $parse is instantiated), for this reason we just have
// the csp.isActive() fn that looks for ng-csp attribute anywhere in the current doc
// the csp() fn that looks for the `ng-csp` attribute anywhere in the current doc
7 changes: 4 additions & 3 deletions src/ng/parse.js
Expand Up @@ -1701,13 +1701,14 @@ function $ParseProvider() {
var cacheDefault = createMap();
var cacheExpensive = createMap();

this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
this.$get = ['$filter', function($filter) {
var noUnsafeEval = csp().noUnsafeEval;
var $parseOptions = {
csp: $sniffer.csp,
csp: noUnsafeEval,
expensiveChecks: false
},
$parseOptionsExpensive = {
csp: $sniffer.csp,
csp: noUnsafeEval,
expensiveChecks: true
};

Expand Down
72 changes: 53 additions & 19 deletions test/AngularSpec.js
Expand Up @@ -785,43 +785,77 @@ describe('angular', function() {


describe('csp', function() {

function mockCspElement(cspAttrName, cspAttrValue) {
return spyOn(document, 'querySelector').andCallFake(function(selector) {
if (selector == '[' + cspAttrName + ']') {
var html = '<div ' + cspAttrName + (cspAttrValue ? ('="' + cspAttrValue + '" ') : '') + '></div>';
return jqLite(html)[0];
}
});

}

var originalFunction;

beforeEach(function() {
originalFunction = window.Function;
spyOn(window, 'Function');
});

afterEach(function() {
window.Function = originalFunction;
delete csp.isActive_;
delete csp.rules;
});


it('should return the false when CSP is not enabled (the default)', function() {
expect(csp()).toBe(false);
it('should return the false for all rules when CSP is not enabled (the default)', function() {
expect(csp()).toEqual({ noUnsafeEval: false, noInlineStyle: false });
});


it('should return true if CSP is autodetected via CSP v1.1 securityPolicy.isActive property', function() {
window.Function = function() { throw new Error('CSP test'); };
expect(csp()).toBe(true);
it('should return true for noUnsafeEval if eval causes a CSP security policy error', function() {
window.Function.andCallFake(function() { throw new Error('CSP test'); });
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: false });
expect(window.Function).toHaveBeenCalledWith('');
});


it('should return the true when CSP is enabled manually via [ng-csp]', function() {
spyOn(document, 'querySelector').andCallFake(function(selector) {
if (selector == '[ng-csp]') return {};
});
expect(csp()).toBe(true);
it('should return true for all rules when CSP is enabled manually via empty `ng-csp` attribute', function() {
var spy = mockCspElement('ng-csp');
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: true });
expect(spy).toHaveBeenCalledWith('[ng-csp]');
expect(window.Function).not.toHaveBeenCalled();
});


it('should return the true when CSP is enabled manually via [data-ng-csp]', function() {
spyOn(document, 'querySelector').andCallFake(function(selector) {
if (selector == '[data-ng-csp]') return {};
});
expect(csp()).toBe(true);
expect(document.querySelector).toHaveBeenCalledWith('[data-ng-csp]');
it('should return true when CSP is enabled manually via [data-ng-csp]', function() {
var spy = mockCspElement('data-ng-csp');
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: true });
expect(spy).toHaveBeenCalledWith('[data-ng-csp]');
expect(window.Function).not.toHaveBeenCalled();
});


it('should return true for noUnsafeEval if it is specified in the `ng-csp` attribute value', function() {
var spy = mockCspElement('ng-csp', 'no-unsafe-eval');
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: false });
expect(spy).toHaveBeenCalledWith('[ng-csp]');
expect(window.Function).not.toHaveBeenCalled();
});


it('should return true for noInlineStyle if it is specified in the `ng-csp` attribute value', function() {
var spy = mockCspElement('ng-csp', 'no-inline-style');
expect(csp()).toEqual({ noUnsafeEval: false, noInlineStyle: true });
expect(spy).toHaveBeenCalledWith('[ng-csp]');
expect(window.Function).not.toHaveBeenCalled();
});


it('should return true for all styles if they are all specified in the `ng-csp` attribute value', function() {
var spy = mockCspElement('ng-csp', 'no-inline-style;no-unsafe-eval');
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: true });
expect(spy).toHaveBeenCalledWith('[ng-csp]');
expect(window.Function).not.toHaveBeenCalled();
});
});

Expand Down
7 changes: 5 additions & 2 deletions test/ng/snifferSpec.js
Expand Up @@ -73,8 +73,11 @@ describe('$sniffer', function() {


describe('csp', function() {
it('should be false by default', function() {
expect(sniffer({}).csp).toBe(false);
it('should have all rules set to false by default', function() {
var csp = sniffer({}).csp;
forEach(Object.keys(csp), function(key) {
expect(csp[key]).toEqual(false);
});
});
});

Expand Down

0 comments on commit 618356e

Please sign in to comment.