Skip to content

Commit

Permalink
feat($http): support sending XSRF token to whitelisted origins
Browse files Browse the repository at this point in the history
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 angular#7862
  • Loading branch information
gkalpak committed Jul 10, 2016
1 parent a82a8a5 commit 0455b09
Show file tree
Hide file tree
Showing 6 changed files with 373 additions and 105 deletions.
1 change: 1 addition & 0 deletions src/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
/* urlUtils.js */
"urlResolve": false,
"urlIsSameOrigin": false,
"urlIsAllowedOriginChecker": false,

/* ng/controller.js */
"identifierForController": false,
Expand Down
98 changes: 80 additions & 18 deletions src/ng/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function $HttpParamSerializerProvider() {
* * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object)
*
* Note that serializer will sort the request parameters alphabetically.
* */
*/

this.$get = function() {
return function ngParamSerializer(params) {
Expand Down Expand Up @@ -103,7 +103,7 @@ function $HttpParamSerializerJQLikeProvider() {
* });
* ```
*
* */
*/
this.$get = function() {
return function jQueryLikeParamSerializer(params) {
if (!params) return '';
Expand Down Expand Up @@ -248,7 +248,7 @@ function isSuccess(status) {
* @name $httpProvider
* @description
* Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service.
* */
*/
function $HttpProvider() {
/**
* @ngdoc property
Expand Down Expand Up @@ -281,7 +281,7 @@ function $HttpProvider() {
* If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}.
* Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}.
*
**/
*/
var defaults = this.defaults = {
// transform incoming response data
transformResponse: [defaultHttpResponseTransform],
Expand Down Expand Up @@ -326,7 +326,7 @@ function $HttpProvider() {
*
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
* otherwise, returns the current configured value.
**/
*/
this.useApplyAsync = function(value) {
if (isDefined(value)) {
useApplyAsync = !!value;
Expand All @@ -350,7 +350,7 @@ function $HttpProvider() {
*
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
* otherwise, returns the current configured value.
**/
*/
this.useLegacyPromiseExtensions = function(value) {
if (isDefined(value)) {
useLegacyPromise = !!value;
Expand All @@ -371,9 +371,49 @@ function $HttpProvider() {
* array, on request, but reverse order, on response.
*
* {@link ng.$http#interceptors Interceptors detailed info}
**/
*/
var interceptorFactories = this.interceptors = [];

/**
* @ngdoc property
* @name $httpProvider#xsrfWhitelistedOrigins
* @description
*
* Array containing URLs whose origins are considered trusted enough to receive the XSRF token.
* See the {@link ng.$http#security-considerations Security Considerations} sections for more
* details on XSRF.
*
* **Note:** An "origin" consists of the [URI scheme](https://en.wikipedia.org/wiki/URI_scheme),
* the [hostname](https://en.wikipedia.org/wiki/Hostname) and the
* [port number](https://en.wikipedia.org/wiki/Port_(computer_networking).
*
* <div class="alert alert-warning">
* It is not possible to whitelist specific URLs/paths. The `path`, `query` and `fragment` parts
* of a URL will be ignored. For example, `https://foo.com/path/bar?query=baz#fragment` will be
* treated as `https://foo.com/`, meaning that **all** requests to URLs starting with
* `https://foo.com/` will include the XSRF token.
* </div>
*
* ## Example
*
* ```
* // App served from `https://example.com`
* angular.
* module('xsrfWhitelistedOriginsExample', []).
* config(['$httpProvider', function($httpProvider) {
* $httpProvider.xsrfWhitelistedOrigins.push('https://api.example.com/');
* }]).
* run(['$http', function($http) {
* // The XSRF token will be sent
* $http.get('https://api.example.com/preferences').then(...);
*
* // The XSRF token will NOT be sent
* $http.get('https://stats.example.com/activity').then(...);
* }]);
* ```
*/
var xsrfWhitelistedOrigins = this.xsrfWhitelistedOrigins = [];

this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector',
function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) {

Expand All @@ -397,6 +437,11 @@ function $HttpProvider() {
? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory));
});

/**
* A function to check request URLs against a list of allowed origins.
*/
var urlIsAllowedOrigin = urlIsAllowedOriginChecker(xsrfWhitelistedOrigins);

/**
* @ngdoc service
* @kind function
Expand Down Expand Up @@ -773,25 +818,42 @@ function $HttpProvider() {
* which the attacker can trick an authenticated user into unknowingly executing actions on your
* website. Angular provides a mechanism to counter XSRF. When performing XHR requests, the
* $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP
* header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the
* cookie, your server can be assured that the XHR came from JavaScript running on your domain.
* The header will not be set for cross-domain requests.
* header (by default `X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read
* the cookie, your server can be assured that the XHR came from JavaScript running on your
* domain.
*
* To take advantage of this, your server needs to set a token in a JavaScript readable session
* cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the
* server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure
* that only JavaScript running on your domain could have sent the request. The token must be
* unique for each user and must be verifiable by the server (to prevent the JavaScript from
* server can verify that the cookie matches the `X-XSRF-TOKEN` HTTP header, and therefore be
* sure that only JavaScript running on your domain could have sent the request. The token must
* be unique for each user and must be verifiable by the server (to prevent the JavaScript from
* making up its own tokens). We recommend that the token is a digest of your site's
* authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography&#41;)
* for added security.
*
* The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName
* properties of either $httpProvider.defaults at config-time, $http.defaults at run-time,
* or the per-request config object.
* The header will &mdash; by default &mdash; **not** be set for cross-domain requests. This
* prevents unauthorized servers (e.g. malicious or compromized 3rd-party APIs) from gaining
* access to your users' XSRF tokens and exposing them to Cross Site Request Forgery. If you
* want to, you can whitelist additional origins to also receive the XSRF token, by adding them
* to {@link ng.$httpProvider#xsrfWhitelistedOrigins xsrfWhitelistedOrigins}. This might be
* useful, for example, if your application, served from `example.com`, needs to access your API
* at `api.example.com`.
* See {@link ng.$httpProvider#xsrfWhitelistedOrigins $httpProvider.xsrfWhitelistedOrigins} for
* more details.
*
* <div class="alert alert-danger">
* **Warning**<br />
* Only whitelist origins that you have control over and make sure you understand the
* implications of doing so.
* </div>
*
* The name of the cookie and the header can be specified using the `xsrfCookieName` and
* `xsrHeaderName` properties of either `$httpProvider.defaults` at config-time,
* `$http.defaults` at run-time, or the per-request config object.
*
* In order to prevent collisions in environments where multiple Angular apps share the
* same domain or subdomain, we recommend that each application uses unique cookie name.
* same domain or subdomain, we recommend that each application uses a unique cookie name.
*
*
* @param {object} config Object describing the request to be made and how it should be
* processed. The object has following properties:
Expand Down Expand Up @@ -1262,7 +1324,7 @@ function $HttpProvider() {
// if we won't have the response in cache, set the xsrf headers and
// send the request to the backend
if (isUndefined(cachedResp)) {
var xsrfValue = urlIsSameOrigin(config.url)
var xsrfValue = urlIsAllowedOrigin(config.url)
? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName]
: undefined;
if (xsrfValue) {
Expand Down
49 changes: 45 additions & 4 deletions src/ng/urlUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ var urlParsingNode = window.document.createElement("a");
var originUrl = urlResolve(window.location.href);


/**
* Compare the origins of two parsed URL objects.
*
* @param {Object} url1 - The first parsed URL object to compare.
* @param {Object} url2 - The second parsed URL object to compare.
*
* @returns {boolean} - Whether the origins of the two URLs are the same.
*/
function sameOrigin(url1, url2) {
return (url1.protocol === url2.protocol) && (url1.host === url2.host);
}

/**
*
* Implementation Notes for non-IE browsers
Expand Down Expand Up @@ -83,14 +95,43 @@ function urlResolve(url) {
}

/**
* Parse a request URL and determine whether this is a same-origin request as the application document.
* Parse a request URL and determine whether this is a same-origin request as the application
* document.
*
* @param {string|object} requestUrl The url of the request as a string that will be resolved
* or a parsed URL object.
* @returns {boolean} Whether the request is for the same origin as the application document.
*/
function urlIsSameOrigin(requestUrl) {
var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
return (parsed.protocol === originUrl.protocol &&
parsed.host === originUrl.host);
var parsedUrl = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
return sameOrigin(parsedUrl, originUrl);
}

/**
* Create a function that can check a URL's origin against a list of allowed/whitelisted origins.
* The current location's origin is implicitly trusted.
*
* @param {string[]} whitelistedOriginUrls - A list of URLs (strings), whose origins are trusted.
*
* @returns {Function} - A function that receives a URL (string or parsed URL object) and returns
* whether it is of an allowed origin.
*/
function urlIsAllowedOriginChecker(whitelistedOriginUrls) {
var parsedAllowedOriginUrls = [originUrl].concat(whitelistedOriginUrls.map(urlResolve));

/**
* Check whether the specified URL (string or parsed URL object) has an origin that is allowed
* based on a list of whitelisted-origin URLs. The current location's origin is implicitly
* trusted.
*
* @param {string|Object} requestUrl - The URL to be checked (provided as a string that will be
* resolved or a parsed URL object).
*
* @returns {boolean} - Whether the specified URL is of an allowed origin.
*/
return function urlIsAllowedOrigin(requestUrl) {
var parsedUrl = isString(requestUrl) ? urlResolve(requestUrl) : requestUrl;

return parsedAllowedOriginUrls.some(sameOrigin.bind(null, parsedUrl));
};
}
1 change: 1 addition & 0 deletions test/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
/* urlUtils.js */
"urlResolve": false,
"urlIsSameOrigin": false,
"urlIsAllowedOriginChecker": false,

/* jasmine / karma */
"it": false,
Expand Down
Loading

0 comments on commit 0455b09

Please sign in to comment.