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
This commit is contained in:
committed by
George Kalpakas
parent
b7d396b8b6
commit
1e90d03077
@@ -161,6 +161,7 @@
|
||||
"urlResolve": false,
|
||||
"urlIsSameOrigin": false,
|
||||
"urlIsSameOriginAsBaseUrl": false,
|
||||
"urlIsAllowedOriginFactory": false,
|
||||
|
||||
/* ng/controller.js */
|
||||
"identifierForController": false,
|
||||
|
||||
+81
-17
@@ -34,7 +34,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) {
|
||||
@@ -101,7 +101,7 @@ function $HttpParamSerializerJQLikeProvider() {
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* */
|
||||
*/
|
||||
this.$get = function() {
|
||||
return function jQueryLikeParamSerializer(params) {
|
||||
if (!params) return '';
|
||||
@@ -261,7 +261,7 @@ function isSuccess(status) {
|
||||
*
|
||||
* @description
|
||||
* Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service.
|
||||
* */
|
||||
*/
|
||||
function $HttpProvider() {
|
||||
/**
|
||||
* @ngdoc property
|
||||
@@ -315,7 +315,7 @@ function $HttpProvider() {
|
||||
* - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the
|
||||
* XSRF token. Defaults value is `'X-XSRF-TOKEN'`.
|
||||
*
|
||||
**/
|
||||
*/
|
||||
var defaults = this.defaults = {
|
||||
// transform incoming response data
|
||||
transformResponse: [defaultHttpResponseTransform],
|
||||
@@ -362,7 +362,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;
|
||||
@@ -383,9 +383,51 @@ 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 trusted 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). For `http:` and
|
||||
* `https:`, the port number can be omitted if using th default ports (80 and 443 respectively).
|
||||
* Examples: `http://example.com`, `https://api.example.com:9876`
|
||||
*
|
||||
* <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
|
||||
*
|
||||
* ```js
|
||||
* // 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 = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', '$sce',
|
||||
function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector, $sce) {
|
||||
|
||||
@@ -409,6 +451,11 @@ function $HttpProvider() {
|
||||
? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory));
|
||||
});
|
||||
|
||||
/**
|
||||
* A function to check request URLs against a list of allowed origins.
|
||||
*/
|
||||
var urlIsAllowedOrigin = urlIsAllowedOriginFactory(xsrfWhitelistedOrigins);
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @kind function
|
||||
@@ -765,25 +812,42 @@ function $HttpProvider() {
|
||||
* which the attacker can trick an authenticated user into unknowingly executing actions on your
|
||||
* website. AngularJS 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))
|
||||
* 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 — by default — **not** be set for cross-domain requests. This
|
||||
* prevents unauthorized servers (e.g. malicious or compromised 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
|
||||
* `xsrfHeaderName` 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 AngularJS 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:
|
||||
@@ -1343,7 +1407,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) {
|
||||
|
||||
+41
-11
@@ -84,7 +84,8 @@ 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.
|
||||
@@ -109,17 +110,46 @@ function urlIsSameOriginAsBaseUrl(requestUrl) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if two URLs share the same origin.
|
||||
* 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|object} url1 First URL to compare as a string or a normalized URL in the form of
|
||||
* @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 urlIsAllowedOriginFactory(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(urlsAreSameOrigin.bind(null, parsedUrl));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if two URLs share the same origin.
|
||||
*
|
||||
* @param {string|Object} url1 - First URL to compare as a string or a normalized URL in the form of
|
||||
* a dictionary object returned by `urlResolve()`.
|
||||
* @param {string|object} url2 Second URL to compare as a string or a normalized URL in the form of
|
||||
* a dictionary object returned by `urlResolve()`.
|
||||
* @return {boolean} True if both URLs have the same origin, and false otherwise.
|
||||
* @param {string|object} url2 - Second URL to compare as a string or a normalized URL in the form
|
||||
* of a dictionary object returned by `urlResolve()`.
|
||||
*
|
||||
* @returns {boolean} - True if both URLs have the same origin, and false otherwise.
|
||||
*/
|
||||
function urlsAreSameOrigin(url1, url2) {
|
||||
url1 = (isString(url1)) ? urlResolve(url1) : url1;
|
||||
url2 = (isString(url2)) ? urlResolve(url2) : url2;
|
||||
url1 = isString(url1) ? urlResolve(url1) : url1;
|
||||
url2 = isString(url2) ? urlResolve(url2) : url2;
|
||||
|
||||
return (url1.protocol === url2.protocol &&
|
||||
url1.host === url2.host);
|
||||
@@ -127,19 +157,19 @@ function urlsAreSameOrigin(url1, url2) {
|
||||
|
||||
/**
|
||||
* Returns the current document base URL.
|
||||
* @return {string}
|
||||
* @returns {string}
|
||||
*/
|
||||
function getBaseUrl() {
|
||||
if (window.document.baseURI) {
|
||||
return window.document.baseURI;
|
||||
}
|
||||
|
||||
// document.baseURI is available everywhere except IE
|
||||
// `document.baseURI` is available everywhere except IE
|
||||
if (!baseUrlParsingNode) {
|
||||
baseUrlParsingNode = window.document.createElement('a');
|
||||
baseUrlParsingNode.href = '.';
|
||||
|
||||
// Work-around for IE bug described in Implementation Notes. The fix in urlResolve() is not
|
||||
// Work-around for IE bug described in Implementation Notes. The fix in `urlResolve()` is not
|
||||
// suitable here because we need to track changes to the base URL.
|
||||
baseUrlParsingNode = baseUrlParsingNode.cloneNode(false);
|
||||
}
|
||||
|
||||
+2
-1
@@ -152,7 +152,8 @@
|
||||
/* urlUtils.js */
|
||||
"urlResolve": false,
|
||||
"urlIsSameOrigin": false,
|
||||
"urlIsSameOriginAsBaseUrl": true,
|
||||
"urlIsSameOriginAsBaseUrl": false,
|
||||
"urlIsAllowedOriginFactory": false,
|
||||
|
||||
/* karma */
|
||||
"dump": false,
|
||||
|
||||
+150
-67
@@ -11,21 +11,16 @@ describe('$http', function() {
|
||||
return Object.keys(params).join('_');
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
callback = jasmine.createSpy('done');
|
||||
mockedCookies = {};
|
||||
module({
|
||||
$$cookieReader: function() {
|
||||
return mockedCookies;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(module({
|
||||
customParamSerializer: customParamSerializer
|
||||
}));
|
||||
beforeEach(module(function($exceptionHandlerProvider) {
|
||||
$exceptionHandlerProvider.mode('log');
|
||||
|
||||
callback = jasmine.createSpy('done');
|
||||
mockedCookies = {};
|
||||
}));
|
||||
|
||||
beforeEach(module({
|
||||
$$cookieReader: function() { return mockedCookies; },
|
||||
customParamSerializer: customParamSerializer
|
||||
}));
|
||||
|
||||
afterEach(inject(function($exceptionHandler, $httpBackend, $rootScope) {
|
||||
@@ -37,7 +32,6 @@ describe('$http', function() {
|
||||
throw 'Unhandled exceptions trapped in $exceptionHandler!';
|
||||
}
|
||||
|
||||
$rootScope.$digest();
|
||||
$httpBackend.verifyNoOutstandingExpectation();
|
||||
}));
|
||||
|
||||
@@ -723,18 +717,6 @@ describe('$http', function() {
|
||||
$httpBackend.flush();
|
||||
});
|
||||
|
||||
it('should not set XSRF cookie for cross-domain requests', inject(function($browser) {
|
||||
mockedCookies['XSRF-TOKEN'] = 'secret';
|
||||
$browser.url('http://host.com/base');
|
||||
$httpBackend.expect('GET', 'http://www.test.com/url', undefined, function(headers) {
|
||||
return isUndefined(headers['X-XSRF-TOKEN']);
|
||||
}).respond('');
|
||||
|
||||
$http({url: 'http://www.test.com/url', method: 'GET', headers: {}});
|
||||
$httpBackend.flush();
|
||||
}));
|
||||
|
||||
|
||||
it('should not send Content-Type header if request data/body is undefined', function() {
|
||||
$httpBackend.expect('POST', '/url', undefined, function(headers) {
|
||||
return !headers.hasOwnProperty('Content-Type');
|
||||
@@ -766,32 +748,6 @@ describe('$http', function() {
|
||||
$httpBackend.flush();
|
||||
});
|
||||
|
||||
it('should set the XSRF cookie into a XSRF header', inject(function() {
|
||||
function checkXSRF(secret, header) {
|
||||
return function(headers) {
|
||||
return headers[header || 'X-XSRF-TOKEN'] === secret;
|
||||
};
|
||||
}
|
||||
|
||||
mockedCookies['XSRF-TOKEN'] = 'secret';
|
||||
mockedCookies['aCookie'] = 'secret2';
|
||||
$httpBackend.expect('GET', '/url', undefined, checkXSRF('secret')).respond('');
|
||||
$httpBackend.expect('POST', '/url', undefined, checkXSRF('secret')).respond('');
|
||||
$httpBackend.expect('PUT', '/url', undefined, checkXSRF('secret')).respond('');
|
||||
$httpBackend.expect('DELETE', '/url', undefined, checkXSRF('secret')).respond('');
|
||||
$httpBackend.expect('GET', '/url', undefined, checkXSRF('secret', 'aHeader')).respond('');
|
||||
$httpBackend.expect('GET', '/url', undefined, checkXSRF('secret2')).respond('');
|
||||
|
||||
$http({url: '/url', method: 'GET'});
|
||||
$http({url: '/url', method: 'POST', headers: {'S-ome': 'Header'}});
|
||||
$http({url: '/url', method: 'PUT', headers: {'Another': 'Header'}});
|
||||
$http({url: '/url', method: 'DELETE', headers: {}});
|
||||
$http({url: '/url', method: 'GET', xsrfHeaderName: 'aHeader'});
|
||||
$http({url: '/url', method: 'GET', xsrfCookieName: 'aCookie'});
|
||||
|
||||
$httpBackend.flush();
|
||||
}));
|
||||
|
||||
it('should send execute result if header value is function', function() {
|
||||
var headerConfig = {'Accept': function() { return 'Rewritten'; }};
|
||||
|
||||
@@ -841,20 +797,6 @@ describe('$http', function() {
|
||||
|
||||
expect(config.foo).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should check the cache before checking the XSRF cookie', inject(function($cacheFactory) {
|
||||
var testCache = $cacheFactory('testCache');
|
||||
|
||||
spyOn(testCache, 'get').and.callFake(function() {
|
||||
mockedCookies['XSRF-TOKEN'] = 'foo';
|
||||
});
|
||||
|
||||
$httpBackend.expect('GET', '/url', undefined, function(headers) {
|
||||
return headers['X-XSRF-TOKEN'] === 'foo';
|
||||
}).respond('');
|
||||
$http({url: '/url', method: 'GET', cache: testCache});
|
||||
$httpBackend.flush();
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -2266,6 +2208,148 @@ describe('$http', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('XSRF', function() {
|
||||
var $http;
|
||||
var $httpBackend;
|
||||
|
||||
beforeEach(module(function($httpProvider) {
|
||||
$httpProvider.xsrfWhitelistedOrigins.push(
|
||||
'https://whitelisted.example.com',
|
||||
'https://whitelisted2.example.com:1337/ignored/path');
|
||||
}));
|
||||
|
||||
beforeEach(inject(function(_$http_, _$httpBackend_) {
|
||||
$http = _$http_;
|
||||
$httpBackend = _$httpBackend_;
|
||||
}));
|
||||
|
||||
|
||||
it('should set the XSRF cookie into an XSRF header', function() {
|
||||
function checkXsrf(secret, header) {
|
||||
return function checkHeaders(headers) {
|
||||
return headers[header || 'X-XSRF-TOKEN'] === secret;
|
||||
};
|
||||
}
|
||||
|
||||
mockedCookies['XSRF-TOKEN'] = 'secret';
|
||||
mockedCookies['aCookie'] = 'secret2';
|
||||
$httpBackend.expect('GET', '/url', null, checkXsrf('secret')).respond(null);
|
||||
$httpBackend.expect('POST', '/url', null, checkXsrf('secret')).respond(null);
|
||||
$httpBackend.expect('PUT', '/url', null, checkXsrf('secret')).respond(null);
|
||||
$httpBackend.expect('DELETE', '/url', null, checkXsrf('secret')).respond(null);
|
||||
$httpBackend.expect('GET', '/url', null, checkXsrf('secret', 'aHeader')).respond(null);
|
||||
$httpBackend.expect('GET', '/url', null, checkXsrf('secret2')).respond(null);
|
||||
|
||||
$http({method: 'GET', url: '/url'});
|
||||
$http({method: 'POST', url: '/url', headers: {'S-ome': 'Header'}});
|
||||
$http({method: 'PUT', url: '/url', headers: {'Another': 'Header'}});
|
||||
$http({method: 'DELETE', url: '/url', headers: {}});
|
||||
$http({method: 'GET', url: '/url', xsrfHeaderName: 'aHeader'});
|
||||
$http({method: 'GET', url: '/url', xsrfCookieName: 'aCookie'});
|
||||
|
||||
$httpBackend.flush();
|
||||
});
|
||||
|
||||
|
||||
it('should support setting a default XSRF cookie/header name', function() {
|
||||
$http.defaults.xsrfCookieName = 'aCookie';
|
||||
$http.defaults.xsrfHeaderName = 'aHeader';
|
||||
|
||||
function checkHeaders(headers) {
|
||||
return headers.aHeader === 'secret';
|
||||
}
|
||||
|
||||
mockedCookies.aCookie = 'secret';
|
||||
$httpBackend.expect('GET', '/url', null, checkHeaders).respond(null);
|
||||
|
||||
$http.get('/url');
|
||||
|
||||
$httpBackend.flush();
|
||||
});
|
||||
|
||||
|
||||
it('should support overriding the default XSRF cookie/header name per request', function() {
|
||||
$http.defaults.xsrfCookieName = 'aCookie';
|
||||
$http.defaults.xsrfHeaderName = 'aHeader';
|
||||
|
||||
function checkHeaders(headers) {
|
||||
return headers.anotherHeader === 'anotherSecret';
|
||||
}
|
||||
|
||||
mockedCookies.anotherCookie = 'anotherSecret';
|
||||
$httpBackend.expect('GET', '/url', null, checkHeaders).respond(null);
|
||||
|
||||
$http.get('/url', {
|
||||
xsrfCookieName: 'anotherCookie',
|
||||
xsrfHeaderName: 'anotherHeader'
|
||||
});
|
||||
|
||||
$httpBackend.flush();
|
||||
});
|
||||
|
||||
|
||||
it('should check the cache before checking the XSRF cookie', inject(function($cacheFactory) {
|
||||
function checkHeaders(headers) {
|
||||
return headers['X-XSRF-TOKEN'] === 'foo';
|
||||
}
|
||||
function setCookie() {
|
||||
mockedCookies['XSRF-TOKEN'] = 'foo';
|
||||
}
|
||||
|
||||
var testCache = $cacheFactory('testCache');
|
||||
spyOn(testCache, 'get').and.callFake(setCookie);
|
||||
|
||||
$httpBackend.expect('GET', '/url', null, checkHeaders).respond(null);
|
||||
$http.get('/url', {cache: testCache});
|
||||
|
||||
$httpBackend.flush();
|
||||
}));
|
||||
|
||||
|
||||
it('should not set an XSRF header for cross-domain requests', function() {
|
||||
function checkHeaders(headers) {
|
||||
return isUndefined(headers['X-XSRF-TOKEN']);
|
||||
}
|
||||
var requestUrls = [
|
||||
'https://api.example.com/path',
|
||||
'http://whitelisted.example.com',
|
||||
'https://whitelisted2.example.com:1338'
|
||||
];
|
||||
|
||||
mockedCookies['XSRF-TOKEN'] = 'secret';
|
||||
|
||||
requestUrls.forEach(function(url) {
|
||||
$httpBackend.expect('GET', url, null, checkHeaders).respond(null);
|
||||
$http.get(url);
|
||||
$httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should set an XSRF header for cross-domain requests to whitelisted origins',
|
||||
inject(function($browser) {
|
||||
function checkHeaders(headers) {
|
||||
return headers['X-XSRF-TOKEN'] === 'secret';
|
||||
}
|
||||
var currentUrl = 'https://example.com/path';
|
||||
var requestUrls = [
|
||||
'https://whitelisted.example.com/path',
|
||||
'https://whitelisted2.example.com:1337/path'
|
||||
];
|
||||
|
||||
$browser.url(currentUrl);
|
||||
mockedCookies['XSRF-TOKEN'] = 'secret';
|
||||
|
||||
requestUrls.forEach(function(url) {
|
||||
$httpBackend.expect('GET', url, null, checkHeaders).respond(null);
|
||||
$http.get(url);
|
||||
$httpBackend.flush();
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should pass timeout, withCredentials and responseType', function() {
|
||||
var $httpBackend = jasmine.createSpy('$httpBackend');
|
||||
|
||||
@@ -2483,7 +2567,6 @@ describe('$http param serializers', function() {
|
||||
'a%5B%5D=b&a%5B%5D=c&d%5B0%5D%5Be%5D=f&d%5B0%5D%5Bg%5D=h&d%5B%5D=i&d%5B2%5D%5Bj%5D=k');
|
||||
//a[]=b&a[]=c&d[0][e]=f&d[0][g]=h&d[]=i&d[2][j]=k
|
||||
});
|
||||
|
||||
it('should serialize `null` and `undefined` elements as empty', function() {
|
||||
expect(jqrSer({items:['foo', 'bar', null, undefined, 'baz'], x: null, y: undefined})).toEqual(
|
||||
'items%5B%5D=foo&items%5B%5D=bar&items%5B%5D=&items%5B%5D=&items%5B%5D=baz&x=&y=');
|
||||
|
||||
+111
-22
@@ -6,6 +6,7 @@ describe('urlUtils', function() {
|
||||
expect(urlResolve('foo').href).toMatch(/^https?:\/\/[^/]+\/foo$/);
|
||||
});
|
||||
|
||||
|
||||
it('should parse relative URL into component pieces', function() {
|
||||
var parsed = urlResolve('foo');
|
||||
expect(parsed.href).toMatch(/https?:\/\//);
|
||||
@@ -23,28 +24,116 @@ describe('urlUtils', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSameOrigin and urlIsSameOriginAsBaseUrl', function() {
|
||||
it('should support various combinations of urls - both string and parsed', inject(function($document) {
|
||||
function expectIsSameOrigin(url, expectedValue) {
|
||||
expect(urlIsSameOrigin(url)).toBe(expectedValue);
|
||||
expect(urlIsSameOrigin(urlResolve(url))).toBe(expectedValue);
|
||||
|
||||
// urlIsSameOriginAsBaseUrl() should behave the same as urlIsSameOrigin() by default.
|
||||
// Behavior when there is a non-default base URL or when the base URL changes dynamically
|
||||
// is tested in the end-to-end tests in e2e/tests/base-tag.spec.js.
|
||||
expect(urlIsSameOriginAsBaseUrl(url)).toBe(expectedValue);
|
||||
expect(urlIsSameOriginAsBaseUrl(urlResolve(url))).toBe(expectedValue);
|
||||
}
|
||||
expectIsSameOrigin('path', true);
|
||||
var origin = urlResolve($document[0].location.href);
|
||||
expectIsSameOrigin('//' + origin.host + '/path', true);
|
||||
// Different domain.
|
||||
expectIsSameOrigin('http://example.com/path', false);
|
||||
// Auto fill protocol.
|
||||
expectIsSameOrigin('//example.com/path', false);
|
||||
// Should not match when the ports are different.
|
||||
// This assumes that the test is *not* running on port 22 (very unlikely).
|
||||
expectIsSameOrigin('//' + origin.hostname + ':22/path', false);
|
||||
}));
|
||||
describe('urlIsSameOrigin and urlIsSameOriginAsBaseUrl', function() {
|
||||
it('should support various combinations of urls - both string and parsed',
|
||||
inject(function($document) {
|
||||
function expectIsSameOrigin(url, expectedValue) {
|
||||
expect(urlIsSameOrigin(url)).toBe(expectedValue);
|
||||
expect(urlIsSameOrigin(urlResolve(url))).toBe(expectedValue);
|
||||
|
||||
// urlIsSameOriginAsBaseUrl() should behave the same as urlIsSameOrigin() by default.
|
||||
// Behavior when there is a non-default base URL or when the base URL changes dynamically
|
||||
// is tested in the end-to-end tests in e2e/tests/base-tag.spec.js.
|
||||
expect(urlIsSameOriginAsBaseUrl(url)).toBe(expectedValue);
|
||||
expect(urlIsSameOriginAsBaseUrl(urlResolve(url))).toBe(expectedValue);
|
||||
}
|
||||
|
||||
expectIsSameOrigin('path', true);
|
||||
|
||||
var origin = urlResolve($document[0].location.href);
|
||||
expectIsSameOrigin('//' + origin.host + '/path', true);
|
||||
|
||||
// Different domain.
|
||||
expectIsSameOrigin('http://example.com/path', false);
|
||||
|
||||
// Auto fill protocol.
|
||||
expectIsSameOrigin('//example.com/path', false);
|
||||
|
||||
// Should not match when the ports are different.
|
||||
// This assumes that the test is *not* running on port 22 (very unlikely).
|
||||
expectIsSameOrigin('//' + origin.hostname + ':22/path', false);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
describe('urlIsAllowedOriginFactory', function() {
|
||||
var origin = urlResolve(window.location.href);
|
||||
var urlIsAllowedOrigin;
|
||||
|
||||
beforeEach(function() {
|
||||
urlIsAllowedOrigin = urlIsAllowedOriginFactory([
|
||||
'https://foo.com/',
|
||||
origin.protocol + '://bar.com:1337/'
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('should implicitly allow the current origin', function() {
|
||||
expect(urlIsAllowedOrigin('path')).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('should check against the list of whitelisted origins', function() {
|
||||
expect(urlIsAllowedOrigin('https://foo.com/path')).toBe(true);
|
||||
expect(urlIsAllowedOrigin(origin.protocol + '://bar.com:1337/path')).toBe(true);
|
||||
expect(urlIsAllowedOrigin('https://baz.com:1337/path')).toBe(false);
|
||||
expect(urlIsAllowedOrigin('https://qux.com/path')).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it('should support both strings and parsed URL objects', function() {
|
||||
expect(urlIsAllowedOrigin('path')).toBe(true);
|
||||
expect(urlIsAllowedOrigin(urlResolve('path'))).toBe(true);
|
||||
expect(urlIsAllowedOrigin('https://foo.com/path')).toBe(true);
|
||||
expect(urlIsAllowedOrigin(urlResolve('https://foo.com/path'))).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('should return true only if the origins (protocol, hostname, post) match', function() {
|
||||
var differentProtocol = (origin.protocol !== 'http') ? 'http' : 'https';
|
||||
var differentPort = (parseInt(origin.port, 10) || 0) + 1;
|
||||
var url;
|
||||
|
||||
|
||||
// Relative path
|
||||
url = 'path';
|
||||
expect(urlIsAllowedOrigin(url)).toBe(true);
|
||||
|
||||
|
||||
// Same origin
|
||||
url = origin.protocol + '://' + origin.host + '/path';
|
||||
expect(urlIsAllowedOrigin(url)).toBe(true);
|
||||
|
||||
// Same origin - implicit protocol
|
||||
url = '//' + origin.host + '/path';
|
||||
expect(urlIsAllowedOrigin(url)).toBe(true);
|
||||
|
||||
// Same origin - different protocol
|
||||
url = differentProtocol + '://' + origin.host + '/path';
|
||||
expect(urlIsAllowedOrigin(url)).toBe(false);
|
||||
|
||||
// Same origin - different port
|
||||
url = origin.protocol + '://' + origin.hostname + ':' + differentPort + '/path';
|
||||
expect(urlIsAllowedOrigin(url)).toBe(false);
|
||||
|
||||
|
||||
// Allowed origin
|
||||
url = origin.protocol + '://bar.com:1337/path';
|
||||
expect(urlIsAllowedOrigin(url)).toBe(true);
|
||||
|
||||
// Allowed origin - implicit protocol
|
||||
url = '//bar.com:1337/path';
|
||||
expect(urlIsAllowedOrigin(url)).toBe(true);
|
||||
|
||||
// Allowed origin - different protocol
|
||||
url = differentProtocol + '://bar.com:1337/path';
|
||||
expect(urlIsAllowedOrigin(url)).toBe(false);
|
||||
|
||||
// Allowed origin - different port
|
||||
url = origin.protocol + '://bar.com:1338/path';
|
||||
expect(urlIsAllowedOrigin(url)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user