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

Commit bc77575

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 fa4451d commit bc77575

File tree

6 files changed

+385
-101
lines changed

6 files changed

+385
-101
lines changed

src/.eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
/* urlUtils.js */
161161
"urlResolve": false,
162162
"urlIsSameOrigin": false,
163+
"urlIsAllowedOriginFactory": false,
163164

164165
/* ng/controller.js */
165166
"identifierForController": false,

src/ng/http.js

+81-17
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function $HttpParamSerializerProvider() {
3434
* * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object)
3535
*
3636
* Note that serializer will sort the request parameters alphabetically.
37-
* */
37+
*/
3838

3939
this.$get = function() {
4040
return function ngParamSerializer(params) {
@@ -101,7 +101,7 @@ function $HttpParamSerializerJQLikeProvider() {
101101
* });
102102
* ```
103103
*
104-
* */
104+
*/
105105
this.$get = function() {
106106
return function jQueryLikeParamSerializer(params) {
107107
if (!params) return '';
@@ -258,7 +258,7 @@ function isSuccess(status) {
258258
*
259259
* @description
260260
* Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service.
261-
* */
261+
*/
262262
function $HttpProvider() {
263263
/**
264264
* @ngdoc property
@@ -312,7 +312,7 @@ function $HttpProvider() {
312312
* - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the
313313
* XSRF token. Defaults value is `'X-XSRF-TOKEN'`.
314314
*
315-
**/
315+
*/
316316
var defaults = this.defaults = {
317317
// transform incoming response data
318318
transformResponse: [defaultHttpResponseTransform],
@@ -359,7 +359,7 @@ function $HttpProvider() {
359359
*
360360
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
361361
* otherwise, returns the current configured value.
362-
**/
362+
*/
363363
this.useApplyAsync = function(value) {
364364
if (isDefined(value)) {
365365
useApplyAsync = !!value;
@@ -380,9 +380,51 @@ function $HttpProvider() {
380380
* array, on request, but reverse order, on response.
381381
*
382382
* {@link ng.$http#interceptors Interceptors detailed info}
383-
**/
383+
*/
384384
var interceptorFactories = this.interceptors = [];
385385

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

@@ -406,6 +448,11 @@ function $HttpProvider() {
406448
? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory));
407449
});
408450

451+
/**
452+
* A function to check request URLs against a list of allowed origins.
453+
*/
454+
var urlIsAllowedOrigin = urlIsAllowedOriginFactory(xsrfWhitelistedOrigins);
455+
409456
/**
410457
* @ngdoc service
411458
* @kind function
@@ -762,25 +809,42 @@ function $HttpProvider() {
762809
* which the attacker can trick an authenticated user into unknowingly executing actions on your
763810
* website. AngularJS provides a mechanism to counter XSRF. When performing XHR requests, the
764811
* $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP
765-
* header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the
766-
* cookie, your server can be assured that the XHR came from JavaScript running on your domain.
767-
* The header will not be set for cross-domain requests.
812+
* header (by default `X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read
813+
* the cookie, your server can be assured that the XHR came from JavaScript running on your
814+
* domain.
768815
*
769816
* To take advantage of this, your server needs to set a token in a JavaScript readable session
770817
* cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the
771-
* server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure
772-
* that only JavaScript running on your domain could have sent the request. The token must be
773-
* unique for each user and must be verifiable by the server (to prevent the JavaScript from
818+
* server can verify that the cookie matches the `X-XSRF-TOKEN` HTTP header, and therefore be
819+
* sure that only JavaScript running on your domain could have sent the request. The token must
820+
* be unique for each user and must be verifiable by the server (to prevent the JavaScript from
774821
* making up its own tokens). We recommend that the token is a digest of your site's
775822
* authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography&#41;)
776823
* for added security.
777824
*
778-
* The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName
779-
* properties of either $httpProvider.defaults at config-time, $http.defaults at run-time,
780-
* or the per-request config object.
825+
* The header will &mdash; by default &mdash; **not** be set for cross-domain requests. This
826+
* prevents unauthorized servers (e.g. malicious or compromised 3rd-party APIs) from gaining
827+
* access to your users' XSRF tokens and exposing them to Cross Site Request Forgery. If you
828+
* want to, you can whitelist additional origins to also receive the XSRF token, by adding them
829+
* to {@link ng.$httpProvider#xsrfWhitelistedOrigins xsrfWhitelistedOrigins}. This might be
830+
* useful, for example, if your application, served from `example.com`, needs to access your API
831+
* at `api.example.com`.
832+
* See {@link ng.$httpProvider#xsrfWhitelistedOrigins $httpProvider.xsrfWhitelistedOrigins} for
833+
* more details.
834+
*
835+
* <div class="alert alert-danger">
836+
* **Warning**<br />
837+
* Only whitelist origins that you have control over and make sure you understand the
838+
* implications of doing so.
839+
* </div>
840+
*
841+
* The name of the cookie and the header can be specified using the `xsrfCookieName` and
842+
* `xsrfHeaderName` properties of either `$httpProvider.defaults` at config-time,
843+
* `$http.defaults` at run-time, or the per-request config object.
781844
*
782845
* In order to prevent collisions in environments where multiple AngularJS apps share the
783-
* same domain or subdomain, we recommend that each application uses unique cookie name.
846+
* same domain or subdomain, we recommend that each application uses a unique cookie name.
847+
*
784848
*
785849
* @param {object} config Object describing the request to be made and how it should be
786850
* processed. The object has following properties:
@@ -1340,7 +1404,7 @@ function $HttpProvider() {
13401404
// if we won't have the response in cache, set the xsrf headers and
13411405
// send the request to the backend
13421406
if (isUndefined(cachedResp)) {
1343-
var xsrfValue = urlIsSameOrigin(config.url)
1407+
var xsrfValue = urlIsAllowedOrigin(config.url)
13441408
? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName]
13451409
: undefined;
13461410
if (xsrfValue) {

src/ng/urlUtils.js

+49-4
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,59 @@ function urlResolve(url) {
8484
}
8585

8686
/**
87-
* Parse a request URL and determine whether this is a same-origin request as the application document.
87+
* Parse a request URL and determine whether this is a same-origin request as the application
88+
* document.
8889
*
8990
* @param {string|object} requestUrl The url of the request as a string that will be resolved
9091
* or a parsed URL object.
9192
* @returns {boolean} Whether the request is for the same origin as the application document.
9293
*/
9394
function urlIsSameOrigin(requestUrl) {
94-
var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
95-
return (parsed.protocol === originUrl.protocol &&
96-
parsed.host === originUrl.host);
95+
return urlsAreSameOrigin(requestUrl, originUrl);
96+
}
97+
98+
/**
99+
* Create a function that can check a URL's origin against a list of allowed/whitelisted origins.
100+
* The current location's origin is implicitly trusted.
101+
*
102+
* @param {string[]} whitelistedOriginUrls - A list of URLs (strings), whose origins are trusted.
103+
*
104+
* @returns {Function} - A function that receives a URL (string or parsed URL object) and returns
105+
* whether it is of an allowed origin.
106+
*/
107+
function urlIsAllowedOriginFactory(whitelistedOriginUrls) {
108+
var parsedAllowedOriginUrls = [originUrl].concat(whitelistedOriginUrls.map(urlResolve));
109+
110+
/**
111+
* Check whether the specified URL (string or parsed URL object) has an origin that is allowed
112+
* based on a list of whitelisted-origin URLs. The current location's origin is implicitly
113+
* trusted.
114+
*
115+
* @param {string|Object} requestUrl - The URL to be checked (provided as a string that will be
116+
* resolved or a parsed URL object).
117+
*
118+
* @returns {boolean} - Whether the specified URL is of an allowed origin.
119+
*/
120+
return function urlIsAllowedOrigin(requestUrl) {
121+
var parsedUrl = isString(requestUrl) ? urlResolve(requestUrl) : requestUrl;
122+
return parsedAllowedOriginUrls.some(urlsAreSameOrigin.bind(null, parsedUrl));
123+
};
124+
}
125+
126+
/**
127+
* Determine if two URLs share the same origin.
128+
*
129+
* @param {string|Object} url1 - First URL to compare as a string or a normalized URL in the form of
130+
* a dictionary object returned by `urlResolve()`.
131+
* @param {string|object} url2 - Second URL to compare as a string or a normalized URL in the form
132+
* of a dictionary object returned by `urlResolve()`.
133+
*
134+
* @returns {boolean} - True if both URLs have the same origin, and false otherwise.
135+
*/
136+
function urlsAreSameOrigin(url1, url2) {
137+
url1 = isString(url1) ? urlResolve(url1) : url1;
138+
url2 = isString(url2) ? urlResolve(url2) : url2;
139+
140+
return (url1.protocol === url2.protocol &&
141+
url1.host === url2.host);
97142
}

test/.eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
/* urlUtils.js */
153153
"urlResolve": false,
154154
"urlIsSameOrigin": false,
155+
"urlIsAllowedOriginFactory": false,
155156

156157
/* karma */
157158
"dump": false,

0 commit comments

Comments
 (0)