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

fix(ngCsp): allow CSP to be configurable #12346

Merged
merged 2 commits into from
Jul 16, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/grunt/utils.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the ngCspAttribute.indexOf checks be looking for > -1 as if they're declared then they're true?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking for !== -1 has the same effect, so this is fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking for !== -1 has the same effect, so this is fine.

Doh! I blame it on all the double negatives 😸

} 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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();
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be also nice to test that no check is performed (i.e. spying on window.Function to verify it hasn't been called or something).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, too



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
Original file line number Diff line number Diff line change
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