diff --git a/src/.eslintrc.json b/src/.eslintrc.json index 64e8b866e6c8..e053a4dfb972 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -161,6 +161,7 @@ "urlResolve": false, "urlIsSameOrigin": false, "urlIsSameOriginAsBaseUrl": false, + "urlIsAllowedOriginFactory": false, /* ng/controller.js */ "identifierForController": false, diff --git a/src/ng/http.js b/src/ng/http.js index 0e44977cfc8b..c8f69ee58ab1 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -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` + * + *
+ * 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. + *
+ * + * @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. + * + *
+ * **Warning**
+ * Only whitelist origins that you have control over and make sure you understand the + * implications of doing so. + *
+ * + * 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) { diff --git a/src/ng/urlUtils.js b/src/ng/urlUtils.js index 2af0c5f6b753..149e14c707b3 100644 --- a/src/ng/urlUtils.js +++ b/src/ng/urlUtils.js @@ -40,7 +40,8 @@ var baseUrlParsingNode; * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ * * @kind function - * @param {string} url The URL to be parsed. + * @param {string|object} url The URL to be parsed. If `url` is not a string, it will be returned + * unchanged. * @description Normalizes and parses a URL. * @returns {object} Returns the normalized URL as a dictionary. * @@ -57,6 +58,8 @@ var baseUrlParsingNode; * */ function urlResolve(url) { + if (!isString(url)) return url; + var href = url; // Support: IE 9-11 only @@ -84,7 +87,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 +113,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 - * 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 + * @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 = urlResolve(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()`. - * @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 = urlResolve(url1); + url2 = urlResolve(url2); return (url1.protocol === url2.protocol && url1.host === url2.host); @@ -127,19 +160,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); } diff --git a/test/.eslintrc.json b/test/.eslintrc.json index 6401cb26f590..28052a8b8af5 100644 --- a/test/.eslintrc.json +++ b/test/.eslintrc.json @@ -152,7 +152,8 @@ /* urlUtils.js */ "urlResolve": false, "urlIsSameOrigin": false, - "urlIsSameOriginAsBaseUrl": true, + "urlIsSameOriginAsBaseUrl": false, + "urlIsAllowedOriginFactory": false, /* karma */ "dump": false, diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index 73a5fccbe44c..f1cf0e896fb5 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -11,22 +11,17 @@ describe('$http', function() { return Object.keys(params).join('_'); }; - beforeEach(function() { + beforeEach(module(function($exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('log'); + callback = jasmine.createSpy('done'); mockedCookies = {}; - module({ - $$cookieReader: function() { - return mockedCookies; - } - }); - }); + })); beforeEach(module({ + $$cookieReader: function() { return mockedCookies; }, customParamSerializer: customParamSerializer })); - beforeEach(module(function($exceptionHandlerProvider) { - $exceptionHandlerProvider.mode('log'); - })); afterEach(inject(function($exceptionHandler, $httpBackend, $rootScope) { forEach($exceptionHandler.errors, function(e) { @@ -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='); diff --git a/test/ng/urlUtilsSpec.js b/test/ng/urlUtilsSpec.js index a13f3661fc5f..ebd864076623 100644 --- a/test/ng/urlUtilsSpec.js +++ b/test/ng/urlUtilsSpec.js @@ -2,10 +2,20 @@ describe('urlUtils', function() { describe('urlResolve', function() { + it('should returned already parsed URLs unchanged', function() { + var urlObj = urlResolve('/foo?bar=baz#qux'); + expect(urlResolve(urlObj)).toBe(urlObj); + expect(urlResolve(true)).toBe(true); + expect(urlResolve(null)).toBeNull(); + expect(urlResolve(undefined)).toBeUndefined(); + }); + + it('should normalize a relative url', 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 +33,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); + }); }); });