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

Commit 00ef7e3

Browse files
committed
feat($http): support sending XSRF token to whitelisted origins
Normally, the XSRF token will not be set for cross-origin requests. With this commit, it is possible to whitelist additional origins, so that requests to these origins will include the XSRF token header. Fixes #7862
1 parent e8d7496 commit 00ef7e3

File tree

6 files changed

+373
-105
lines changed

6 files changed

+373
-105
lines changed

src/.eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
/* urlUtils.js */
155155
"urlResolve": false,
156156
"urlIsSameOrigin": false,
157+
"urlIsAllowedOriginChecker": false,
157158

158159
/* ng/controller.js */
159160
"identifierForController": false,

src/ng/http.js

+80-18
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function $HttpParamSerializerProvider() {
3939
* * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object)
4040
*
4141
* Note that serializer will sort the request parameters alphabetically.
42-
* */
42+
*/
4343

4444
this.$get = function() {
4545
return function ngParamSerializer(params) {
@@ -106,7 +106,7 @@ function $HttpParamSerializerJQLikeProvider() {
106106
* });
107107
* ```
108108
*
109-
* */
109+
*/
110110
this.$get = function() {
111111
return function jQueryLikeParamSerializer(params) {
112112
if (!params) return '';
@@ -253,7 +253,7 @@ function isSuccess(status) {
253253
*
254254
* @description
255255
* Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service.
256-
* */
256+
*/
257257
function $HttpProvider() {
258258
/**
259259
* @ngdoc property
@@ -286,7 +286,7 @@ function $HttpProvider() {
286286
* If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}.
287287
* Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}.
288288
*
289-
**/
289+
*/
290290
var defaults = this.defaults = {
291291
// transform incoming response data
292292
transformResponse: [defaultHttpResponseTransform],
@@ -331,7 +331,7 @@ function $HttpProvider() {
331331
*
332332
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
333333
* otherwise, returns the current configured value.
334-
**/
334+
*/
335335
this.useApplyAsync = function(value) {
336336
if (isDefined(value)) {
337337
useApplyAsync = !!value;
@@ -355,7 +355,7 @@ function $HttpProvider() {
355355
*
356356
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
357357
* otherwise, returns the current configured value.
358-
**/
358+
*/
359359
this.useLegacyPromiseExtensions = function(value) {
360360
if (isDefined(value)) {
361361
useLegacyPromise = !!value;
@@ -376,9 +376,49 @@ function $HttpProvider() {
376376
* array, on request, but reverse order, on response.
377377
*
378378
* {@link ng.$http#interceptors Interceptors detailed info}
379-
**/
379+
*/
380380
var interceptorFactories = this.interceptors = [];
381381

382+
/**
383+
* @ngdoc property
384+
* @name $httpProvider#xsrfWhitelistedOrigins
385+
* @description
386+
*
387+
* Array containing URLs whose origins are considered trusted enough to receive the XSRF token.
388+
* See the {@link ng.$http#security-considerations Security Considerations} sections for more
389+
* details on XSRF.
390+
*
391+
* **Note:** An "origin" consists of the [URI scheme](https://en.wikipedia.org/wiki/URI_scheme),
392+
* the [hostname](https://en.wikipedia.org/wiki/Hostname) and the
393+
* [port number](https://en.wikipedia.org/wiki/Port_(computer_networking).
394+
*
395+
* <div class="alert alert-warning">
396+
* It is not possible to whitelist specific URLs/paths. The `path`, `query` and `fragment` parts
397+
* of a URL will be ignored. For example, `https://foo.com/path/bar?query=baz#fragment` will be
398+
* treated as `https://foo.com/`, meaning that **all** requests to URLs starting with
399+
* `https://foo.com/` will include the XSRF token.
400+
* </div>
401+
*
402+
* ## Example
403+
*
404+
* ```
405+
* // App served from `https://example.com`
406+
* angular.
407+
* module('xsrfWhitelistedOriginsExample', []).
408+
* config(['$httpProvider', function($httpProvider) {
409+
* $httpProvider.xsrfWhitelistedOrigins.push('https://api.example.com/');
410+
* }]).
411+
* run(['$http', function($http) {
412+
* // The XSRF token will be sent
413+
* $http.get('https://api.example.com/preferences').then(...);
414+
*
415+
* // The XSRF token will NOT be sent
416+
* $http.get('https://stats.example.com/activity').then(...);
417+
* }]);
418+
* ```
419+
*/
420+
var xsrfWhitelistedOrigins = this.xsrfWhitelistedOrigins = [];
421+
382422
this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector',
383423
function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) {
384424

@@ -402,6 +442,11 @@ function $HttpProvider() {
402442
? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory));
403443
});
404444

445+
/**
446+
* A function to check request URLs against a list of allowed origins.
447+
*/
448+
var urlIsAllowedOrigin = urlIsAllowedOriginChecker(xsrfWhitelistedOrigins);
449+
405450
/**
406451
* @ngdoc service
407452
* @kind function
@@ -778,25 +823,42 @@ function $HttpProvider() {
778823
* which the attacker can trick an authenticated user into unknowingly executing actions on your
779824
* website. Angular provides a mechanism to counter XSRF. When performing XHR requests, the
780825
* $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP
781-
* header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the
782-
* cookie, your server can be assured that the XHR came from JavaScript running on your domain.
783-
* The header will not be set for cross-domain requests.
826+
* header (by default `X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read
827+
* the cookie, your server can be assured that the XHR came from JavaScript running on your
828+
* domain.
784829
*
785830
* To take advantage of this, your server needs to set a token in a JavaScript readable session
786831
* cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the
787-
* server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure
788-
* that only JavaScript running on your domain could have sent the request. The token must be
789-
* unique for each user and must be verifiable by the server (to prevent the JavaScript from
832+
* server can verify that the cookie matches the `X-XSRF-TOKEN` HTTP header, and therefore be
833+
* sure that only JavaScript running on your domain could have sent the request. The token must
834+
* be unique for each user and must be verifiable by the server (to prevent the JavaScript from
790835
* making up its own tokens). We recommend that the token is a digest of your site's
791836
* authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography&#41;)
792837
* for added security.
793838
*
794-
* The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName
795-
* properties of either $httpProvider.defaults at config-time, $http.defaults at run-time,
796-
* or the per-request config object.
839+
* The header will &mdash; by default &mdash; **not** be set for cross-domain requests. This
840+
* prevents unauthorized servers (e.g. malicious or compromized 3rd-party APIs) from gaining
841+
* access to your users' XSRF tokens and exposing them to Cross Site Request Forgery. If you
842+
* want to, you can whitelist additional origins to also receive the XSRF token, by adding them
843+
* to {@link ng.$httpProvider#xsrfWhitelistedOrigins xsrfWhitelistedOrigins}. This might be
844+
* useful, for example, if your application, served from `example.com`, needs to access your API
845+
* at `api.example.com`.
846+
* See {@link ng.$httpProvider#xsrfWhitelistedOrigins $httpProvider.xsrfWhitelistedOrigins} for
847+
* more details.
848+
*
849+
* <div class="alert alert-danger">
850+
* **Warning**<br />
851+
* Only whitelist origins that you have control over and make sure you understand the
852+
* implications of doing so.
853+
* </div>
854+
*
855+
* The name of the cookie and the header can be specified using the `xsrfCookieName` and
856+
* `xsrHeaderName` properties of either `$httpProvider.defaults` at config-time,
857+
* `$http.defaults` at run-time, or the per-request config object.
797858
*
798859
* In order to prevent collisions in environments where multiple Angular apps share the
799-
* same domain or subdomain, we recommend that each application uses unique cookie name.
860+
* same domain or subdomain, we recommend that each application uses a unique cookie name.
861+
*
800862
*
801863
* @param {object} config Object describing the request to be made and how it should be
802864
* processed. The object has following properties:
@@ -1286,7 +1348,7 @@ function $HttpProvider() {
12861348
// if we won't have the response in cache, set the xsrf headers and
12871349
// send the request to the backend
12881350
if (isUndefined(cachedResp)) {
1289-
var xsrfValue = urlIsSameOrigin(config.url)
1351+
var xsrfValue = urlIsAllowedOrigin(config.url)
12901352
? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName]
12911353
: undefined;
12921354
if (xsrfValue) {

src/ng/urlUtils.js

+45-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ var urlParsingNode = window.document.createElement("a");
1010
var originUrl = urlResolve(window.location.href);
1111

1212

13+
/**
14+
* Compare the origins of two parsed URL objects.
15+
*
16+
* @param {Object} url1 - The first parsed URL object to compare.
17+
* @param {Object} url2 - The second parsed URL object to compare.
18+
*
19+
* @returns {boolean} - Whether the origins of the two URLs are the same.
20+
*/
21+
function sameOrigin(url1, url2) {
22+
return (url1.protocol === url2.protocol) && (url1.host === url2.host);
23+
}
24+
1325
/**
1426
*
1527
* Implementation Notes for non-IE browsers
@@ -83,14 +95,43 @@ function urlResolve(url) {
8395
}
8496

8597
/**
86-
* Parse a request URL and determine whether this is a same-origin request as the application document.
98+
* Parse a request URL and determine whether this is a same-origin request as the application
99+
* document.
87100
*
88101
* @param {string|object} requestUrl The url of the request as a string that will be resolved
89102
* or a parsed URL object.
90103
* @returns {boolean} Whether the request is for the same origin as the application document.
91104
*/
92105
function urlIsSameOrigin(requestUrl) {
93-
var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
94-
return (parsed.protocol === originUrl.protocol &&
95-
parsed.host === originUrl.host);
106+
var parsedUrl = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
107+
return sameOrigin(parsedUrl, originUrl);
108+
}
109+
110+
/**
111+
* Create a function that can check a URL's origin against a list of allowed/whitelisted origins.
112+
* The current location's origin is implicitly trusted.
113+
*
114+
* @param {string[]} whitelistedOriginUrls - A list of URLs (strings), whose origins are trusted.
115+
*
116+
* @returns {Function} - A function that receives a URL (string or parsed URL object) and returns
117+
* whether it is of an allowed origin.
118+
*/
119+
function urlIsAllowedOriginChecker(whitelistedOriginUrls) {
120+
var parsedAllowedOriginUrls = [originUrl].concat(whitelistedOriginUrls.map(urlResolve));
121+
122+
/**
123+
* Check whether the specified URL (string or parsed URL object) has an origin that is allowed
124+
* based on a list of whitelisted-origin URLs. The current location's origin is implicitly
125+
* trusted.
126+
*
127+
* @param {string|Object} requestUrl - The URL to be checked (provided as a string that will be
128+
* resolved or a parsed URL object).
129+
*
130+
* @returns {boolean} - Whether the specified URL is of an allowed origin.
131+
*/
132+
return function urlIsAllowedOrigin(requestUrl) {
133+
var parsedUrl = isString(requestUrl) ? urlResolve(requestUrl) : requestUrl;
134+
135+
return parsedAllowedOriginUrls.some(sameOrigin.bind(null, parsedUrl));
136+
};
96137
}

test/.eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
/* urlUtils.js */
145145
"urlResolve": false,
146146
"urlIsSameOrigin": false,
147+
"urlIsAllowedOriginChecker": false,
147148

148149
/* karma */
149150
"dump": false,

0 commit comments

Comments
 (0)