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

Commit 618356e

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 Closes #12346
1 parent 1f4aa47 commit 618356e

File tree

7 files changed

+134
-50
lines changed

7 files changed

+134
-50
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

+47-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,39 @@
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>`. This is backwardly compatible with previous versions
57+
* of Angular.
58+
*
59+
* * A simple `ng-csp` (or `data-ng-csp`) attribute will tell Angular to deactivate both inline
60+
* styles and unsafe eval. E.g. `<body ng-csp>`. This is backwardly compatible with previous versions
61+
* of Angular.
62+
*
63+
* * Specifying only `no-unsafe-eval` tells Angular that we must not use eval, but that we can inject
64+
* inline styles. E.g. `<body ng-csp="no-unsafe-eval">`.
65+
*
66+
* * Specifying only `no-inline-style` tells Angular that we must not inject styles, but that we can
67+
* run eval - no automcatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">`
68+
*
69+
* * Specifying both `no-unsafe-eval` and `no-inline-style` tells Angular that we must not inject
70+
* styles nor use eval, which is the same as an empty: ng-csp.
71+
* E.g.`<body ng-csp="no-inline-style;no-unsafe-eval">`
72+
*
4373
* @example
4474
* This example shows how to apply the `ngCsp` directive to the `html` tag.
4575
```html
@@ -171,4 +201,4 @@
171201

172202
// ngCsp is not implemented as a proper directive any more, because we need it be processed while we
173203
// 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
204+
// the csp() fn that looks for the `ng-csp` attribute anywhere in the current doc

src/ng/parse.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -1701,13 +1701,14 @@ function $ParseProvider() {
17011701
var cacheDefault = createMap();
17021702
var cacheExpensive = createMap();
17031703

1704-
this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
1704+
this.$get = ['$filter', function($filter) {
1705+
var noUnsafeEval = csp().noUnsafeEval;
17051706
var $parseOptions = {
1706-
csp: $sniffer.csp,
1707+
csp: noUnsafeEval,
17071708
expensiveChecks: false
17081709
},
17091710
$parseOptionsExpensive = {
1710-
csp: $sniffer.csp,
1711+
csp: noUnsafeEval,
17111712
expensiveChecks: true
17121713
};
17131714

test/AngularSpec.js

+53-19
Original file line numberDiff line numberDiff line change
@@ -785,43 +785,77 @@ 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() {
791-
originalFunction = window.Function;
802+
spyOn(window, 'Function');
792803
});
793804

794805
afterEach(function() {
795-
window.Function = originalFunction;
796-
delete csp.isActive_;
806+
delete csp.rules;
797807
});
798808

799809

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

804814

805-
it('should return true if CSP is autodetected via CSP v1.1 securityPolicy.isActive property', function() {
806-
window.Function = function() { throw new Error('CSP test'); };
807-
expect(csp()).toBe(true);
815+
it('should return true for noUnsafeEval if eval causes a CSP security policy error', function() {
816+
window.Function.andCallFake(function() { throw new Error('CSP test'); });
817+
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: false });
818+
expect(window.Function).toHaveBeenCalledWith('');
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]');
826+
expect(window.Function).not.toHaveBeenCalled();
816827
});
817828

818829

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);
824-
expect(document.querySelector).toHaveBeenCalledWith('[data-ng-csp]');
830+
it('should return true when CSP is enabled manually via [data-ng-csp]', function() {
831+
var spy = mockCspElement('data-ng-csp');
832+
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: true });
833+
expect(spy).toHaveBeenCalledWith('[data-ng-csp]');
834+
expect(window.Function).not.toHaveBeenCalled();
835+
});
836+
837+
838+
it('should return true for noUnsafeEval if it is specified in the `ng-csp` attribute value', function() {
839+
var spy = mockCspElement('ng-csp', 'no-unsafe-eval');
840+
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: false });
841+
expect(spy).toHaveBeenCalledWith('[ng-csp]');
842+
expect(window.Function).not.toHaveBeenCalled();
843+
});
844+
845+
846+
it('should return true for noInlineStyle if it is specified in the `ng-csp` attribute value', function() {
847+
var spy = mockCspElement('ng-csp', 'no-inline-style');
848+
expect(csp()).toEqual({ noUnsafeEval: false, noInlineStyle: true });
849+
expect(spy).toHaveBeenCalledWith('[ng-csp]');
850+
expect(window.Function).not.toHaveBeenCalled();
851+
});
852+
853+
854+
it('should return true for all styles if they are all specified in the `ng-csp` attribute value', function() {
855+
var spy = mockCspElement('ng-csp', 'no-inline-style;no-unsafe-eval');
856+
expect(csp()).toEqual({ noUnsafeEval: true, noInlineStyle: true });
857+
expect(spy).toHaveBeenCalledWith('[ng-csp]');
858+
expect(window.Function).not.toHaveBeenCalled();
825859
});
826860
});
827861

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)