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

Commit ae22415

Browse files
fix(ngCsp): allow CSP to be configurable
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
1 parent 4cef752 commit ae22415

File tree

7 files changed

+118
-45
lines changed

7 files changed

+118
-45
lines changed

lib/grunt/utils.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ module.exports = {
116116
.replace(/\\/g, '\\\\')
117117
.replace(/'/g, "\\'")
118118
.replace(/\r?\n/g, '\\n');
119-
js = "!window.angular.$$csp() && window.angular.element(document.head).prepend('<style type=\"text/css\">" + css + "</style>');";
119+
js = "!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend('<style type=\"text/css\">" + css + "</style>');";
120120
state.js.push(js);
121121

122122
return state;

src/Angular.js

+24-7
Original file line numberDiff line numberDiff line change
@@ -984,22 +984,39 @@ function equals(o1, o2) {
984984
}
985985

986986
var csp = function() {
987-
if (isDefined(csp.isActive_)) return csp.isActive_;
987+
if (!isDefined(csp.rules)) {
988988

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

992-
if (!active) {
990+
var ngCspElement = (document.querySelector('[ng-csp]') ||
991+
document.querySelector('[data-ng-csp]'));
992+
993+
if (ngCspElement) {
994+
var ngCspAttribute = ngCspElement.getAttribute('ng-csp') ||
995+
ngCspElement.getAttribute('data-ng-csp');
996+
csp.rules = {
997+
noUnsafeEval: !ngCspAttribute || (ngCspAttribute.indexOf('no-unsafe-eval') !== -1),
998+
noInlineStyle: !ngCspAttribute || (ngCspAttribute.indexOf('no-inline-style') !== -1)
999+
};
1000+
} else {
1001+
csp.rules = {
1002+
noUnsafeEval: noUnsafeEval(),
1003+
noInlineStyle: false
1004+
};
1005+
}
1006+
}
1007+
1008+
return csp.rules;
1009+
1010+
function noUnsafeEval() {
9931011
try {
9941012
/* jshint -W031, -W054 */
9951013
new Function('');
9961014
/* jshint +W031, +W054 */
1015+
return false;
9971016
} catch (e) {
998-
active = true;
1017+
return true;
9991018
}
10001019
}
1001-
1002-
return (csp.isActive_ = active);
10031020
};
10041021

10051022
/**

src/AngularPublic.js

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
ngClassDirective,
2121
ngClassEvenDirective,
2222
ngClassOddDirective,
23-
ngCspDirective,
2423
ngCloakDirective,
2524
ngControllerDirective,
2625
ngFormDirective,

src/ng/directive/ngCsp.js

+45-17
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,29 @@
66
*
77
* @element html
88
* @description
9-
* Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support.
9+
*
10+
* Angular has some features that can break certain
11+
* [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules.
12+
*
13+
* If you intend to implement these rules then you must tell Angular not to use these features.
1014
*
1115
* This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps.
1216
*
13-
* CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things).
14-
* For Angular to be CSP compatible there are only two things that we need to do differently:
1517
*
16-
* - don't use `Function` constructor to generate optimized value getters
17-
* - don't inject custom stylesheet into the document
18+
* The following rules affect Angular:
1819
*
19-
* AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp`
20-
* directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will
21-
* evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will
22-
* be raised.
20+
* * `unsafe-eval`: this rule forbids apps to use `eval` or `Function(string)` generated functions
21+
* (among other things). Angular makes use of this in the {@link $parse} service to provide a 30%
22+
* increase in the speed of evaluating Angular expressions.
2323
*
24-
* CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically
25-
* includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}).
26-
* To make those directives work in CSP mode, include the `angular-csp.css` manually.
24+
* * `unsafe-inline`: this rule forbids apps from inject custom styles into the document. Angular
25+
* makes use of this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}).
26+
* To make these directives work when a CSP rule is blocking inline styles, you must link to the
27+
* `angular-csp.css` in your HTML manually.
2728
*
28-
* Angular tries to autodetect if CSP is active and automatically turn on the CSP-safe mode. This
29-
* autodetection however triggers a CSP error to be logged in the console:
29+
* If you do not provide `ngCsp` then Angular tries to autodetect if CSP is blocking unsafe-eval
30+
* and automatically deactivates this feature in the {@link $parse} service. This autodetection,
31+
* however, triggers a CSP error to be logged in the console:
3032
*
3133
* ```
3234
* Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of
@@ -35,11 +37,37 @@
3537
* ```
3638
*
3739
* This error is harmless but annoying. To prevent the error from showing up, put the `ngCsp`
38-
* directive on the root element of the application or on the `angular.js` script tag, whichever
39-
* appears first in the html document.
40+
* directive on an element of the HTML document that appears before the `<script>` tag that loads
41+
* the `angular.js` file.
4042
*
4143
* *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.*
4244
*
45+
* You can specify which of the CSP related Angular features should be deactivated by providing
46+
* a value for the `ng-csp` attribute. The options are as follows:
47+
*
48+
* * no-inline-style: this stops Angular from injecting CSS styles into the DOM
49+
*
50+
* * no-unsafe-eval: this stops Angular from optimising $parse with unsafe eval of strings
51+
*
52+
* You can use these values in the following combinations:
53+
*
54+
*
55+
* * No declaration means that Angular will assume that you can do inline styles, but it will do
56+
* a runtime check for unsafe-eval. E.g. `<body>`.
57+
*
58+
* * A simple `ng-csp` (or `data-ng-csp`) attribute will tell Angular to deactivate both inline
59+
* styles and unsafe eval. E.g. `<body ng-csp>`.
60+
*
61+
* * Specifying only `no-unsafe-eval` tells Angular that we must not use eval, but that we can inject
62+
* inline styles. E.g. `<body ng-csp="no-unsafe-eval">`.
63+
*
64+
* * Specifying only `no-inline-style` tells Angular that we must not inject styles, but that we can
65+
* run eval - no automcatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">`
66+
*
67+
* * Specifying both `no-unsafe-eval` and `no-inline-style` tells Angular that we must not inject
68+
* styles nor use eval, which is the same as an empty: ng-csp, except that no runtime check for
69+
* unsafe eval will occur. E.g.`<body ng-csp="no-inline-style;no-unsafe-eval">`
70+
*
4371
* @example
4472
* This example shows how to apply the `ngCsp` directive to the `html` tag.
4573
```html
@@ -171,4 +199,4 @@
171199

172200
// ngCsp is not implemented as a proper directive any more, because we need it be processed while we
173201
// bootstrap the system (before $parse is instantiated), for this reason we just have
174-
// the csp.isActive() fn that looks for ng-csp attribute anywhere in the current doc
202+
// the csp() fn that looks for the `ng-csp` attribute anywhere in the current doc

src/ng/parse.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1703,11 +1703,11 @@ function $ParseProvider() {
17031703

17041704
this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
17051705
var $parseOptions = {
1706-
csp: $sniffer.csp,
1706+
csp: $sniffer.csp && $sniffer.csp.noUnsafeEval,
17071707
expensiveChecks: false
17081708
},
17091709
$parseOptionsExpensive = {
1710-
csp: $sniffer.csp,
1710+
csp: $sniffer.csp && $sniffer.csp.noUnsafeEval,
17111711
expensiveChecks: true
17121712
};
17131713

test/AngularSpec.js

+41-15
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,17 @@ describe('angular', function() {
785785

786786

787787
describe('csp', function() {
788+
789+
function mockCspElement(cspAttrName, cspAttrValue) {
790+
return spyOn(document, 'querySelector').andCallFake(function(selector) {
791+
if (selector == '[' + cspAttrName + ']') {
792+
var html = '<div ' + cspAttrName + (cspAttrValue ? ('="' + cspAttrValue + '" ') : '') + '></div>';
793+
return jqLite(html)[0];
794+
}
795+
});
796+
797+
}
798+
788799
var originalFunction;
789800

790801
beforeEach(function() {
@@ -793,36 +804,51 @@ describe('angular', function() {
793804

794805
afterEach(function() {
795806
window.Function = originalFunction;
796-
delete csp.isActive_;
807+
delete csp.rules;
797808
});
798809

799810

800-
it('should return the false when CSP is not enabled (the default)', function() {
801-
expect(csp()).toBe(false);
811+
it('should return the false for all rules when CSP is not enabled (the default)', function() {
812+
expect(csp()).toEqual({ noUnsafeEval: false, noInlineStyle: false });
802813
});
803814

804815

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

810821

811-
it('should return the true when CSP is enabled manually via [ng-csp]', function() {
812-
spyOn(document, 'querySelector').andCallFake(function(selector) {
813-
if (selector == '[ng-csp]') return {};
814-
});
815-
expect(csp()).toBe(true);
822+
it('should return true for all rules when CSP is enabled manually via empty `ng-csp` attribute', function() {
823+
var spy = mockCspElement('ng-csp');
824+
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: true });
825+
expect(spy).toHaveBeenCalledWith('[ng-csp]');
816826
});
817827

818828

819-
it('should return the true when CSP is enabled manually via [data-ng-csp]', function() {
820-
spyOn(document, 'querySelector').andCallFake(function(selector) {
821-
if (selector == '[data-ng-csp]') return {};
822-
});
823-
expect(csp()).toBe(true);
829+
it('should return true when CSP is enabled manually via [data-ng-csp]', function() {
830+
var spy = mockCspElement('data-ng-csp');
831+
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: true });
824832
expect(document.querySelector).toHaveBeenCalledWith('[data-ng-csp]');
825833
});
834+
835+
836+
it('should return true for noUnsafeEval if it is specified in the `ng-csp` attribute value', function() {
837+
var spy = mockCspElement('ng-csp', 'no-unsafe-eval');
838+
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: false });
839+
});
840+
841+
842+
it('should return true for noInlineStyle if it is specified in the `ng-csp` attribute value', function() {
843+
var spy = mockCspElement('ng-csp', 'no-inline-style');
844+
expect(csp()).toEqual({ noUnsafeEval: false, noInlineStyle: true });
845+
});
846+
847+
848+
it('should return true for all styles if they are all specified in the `ng-csp` attribute value', function() {
849+
var spy = mockCspElement('ng-csp', 'no-inline-style;no-unsafe-eval');
850+
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: true });
851+
});
826852
});
827853

828854

test/ng/snifferSpec.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,11 @@ describe('$sniffer', function() {
7373

7474

7575
describe('csp', function() {
76-
it('should be false by default', function() {
77-
expect(sniffer({}).csp).toBe(false);
76+
it('should have all rules set to false by default', function() {
77+
var csp = sniffer({}).csp;
78+
forEach(Object.keys(csp), function(key) {
79+
expect(csp[key]).toEqual(false);
80+
});
7881
});
7982
});
8083

0 commit comments

Comments
 (0)