AngularJS has some features that can conflict with certain restrictions that are applied when using CSP (Content Security Policy) rules.
If you intend to implement CSP with these rules then you must tell AngularJS not to use these features.
This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps.
The following default rules in CSP affect AngularJS:
The use of eval()
, Function(string)
and similar functions to dynamically create and execute
code from strings is forbidden. AngularJS makes use of this in the $parse
service to
provide a 30% increase in the speed of evaluating AngularJS expressions. (This CSP rule can be
disabled with the CSP keyword unsafe-eval
, but it is generally not recommended as it would
weaken the protections offered by CSP.)
The use of inline resources, such as inline <script>
and <style>
elements, are forbidden.
This prevents apps from injecting custom styles directly into the document. AngularJS makes use of
this to include some CSS rules (e.g. ngCloak
and 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. (This CSP rule can be disabled with the CSP keyword unsafe-inline
, but
it is generally not recommended as it would weaken the protections offered by CSP.)
If you do not provide ngCsp
then AngularJS tries to autodetect if CSP is blocking dynamic code
creation from strings (e.g., unsafe-eval
not specified in CSP header) and automatically
deactivates this feature in the $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
script in the following Content Security Policy directive: "default-src 'self'". Note that
'script-src' was not explicitly set, so 'default-src' is used as a fallback.
This error is harmless but annoying. To prevent the error from showing up, put the ngCsp
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 AngularJS features should be deactivated by providing
a value for the ng-csp
attribute. The options are as follows:
no-inline-style: this stops AngularJS from injecting CSS styles into the DOM
no-unsafe-eval: this stops AngularJS from optimizing $parse with unsafe eval of strings
You can use these values in the following combinations:
No declaration means that AngularJS 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 AngularJS.
A simple ng-csp
(or data-ng-csp
) attribute will tell AngularJS to deactivate both inline
styles and unsafe eval. E.g. <body ng-csp>
. This is backwardly compatible with previous
versions of AngularJS.
Specifying only no-unsafe-eval
tells AngularJS 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 AngularJS that we must not inject styles, but that we can
run eval - no automatic check for unsafe eval will occur. E.g. <body ng-csp="no-inline-style">
Specifying both no-unsafe-eval
and no-inline-style
tells AngularJS 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">
<ANY
ng-csp>
...
</ANY>
This example shows how to apply the ngCsp
directive to the html
tag.
<!doctype html>
<html ng-app ng-csp>
...
...
</html>
<div ng-controller="MainController as ctrl">
<div>
<button ng-click="ctrl.inc()" id="inc">Increment</button>
<span id="counter">
{{ctrl.counter}}
</span>
</div>
<div>
<button ng-click="ctrl.evil()" id="evil">Evil</button>
<span id="evilError">
{{ctrl.evilError}}
</span>
</div>
</div>
angular.module('cspExample', [])
.controller('MainController', function MainController() {
this.counter = 0;
this.inc = function() {
this.counter++;
};
this.evil = function() {
try {
eval('1+2'); // eslint-disable-line no-eval
} catch (e) {
this.evilError = e.message;
}
};
});
var util, webdriver;
var incBtn = element(by.id('inc'));
var counter = element(by.id('counter'));
var evilBtn = element(by.id('evil'));
var evilError = element(by.id('evilError'));
function getAndClearSevereErrors() {
return browser.manage().logs().get('browser').then(function(browserLog) {
return browserLog.filter(function(logEntry) {
return logEntry.level.value > webdriver.logging.Level.WARNING.value;
});
});
}
function clearErrors() {
getAndClearSevereErrors();
}
function expectNoErrors() {
getAndClearSevereErrors().then(function(filteredLog) {
expect(filteredLog.length).toEqual(0);
if (filteredLog.length) {
console.log('browser console errors: ' + util.inspect(filteredLog));
}
});
}
function expectError(regex) {
getAndClearSevereErrors().then(function(filteredLog) {
var found = false;
filteredLog.forEach(function(log) {
if (log.message.match(regex)) {
found = true;
}
});
if (!found) {
throw new Error('expected an error that matches ' + regex);
}
});
}
beforeEach(function() {
util = require('util');
webdriver = require('selenium-webdriver');
});
// For now, we only test on Chrome,
// as Safari does not load the page with Protractor's injected scripts,
// and Firefox webdriver always disables content security policy (#6358)
if (browser.params.browser !== 'chrome') {
return;
}
it('should not report errors when the page is loaded', function() {
// clear errors so we are not dependent on previous tests
clearErrors();
// Need to reload the page as the page is already loaded when
// we come here
browser.driver.getCurrentUrl().then(function(url) {
browser.get(url);
});
expectNoErrors();
});
it('should evaluate expressions', function() {
expect(counter.getText()).toEqual('0');
incBtn.click();
expect(counter.getText()).toEqual('1');
expectNoErrors();
});
it('should throw and report an error when using "eval"', function() {
evilBtn.click();
expect(evilError.getText()).toMatch(/Content Security Policy/);
expectError(/Content Security Policy/);
});