diff --git a/src/.eslintrc.json b/src/.eslintrc.json index caa332ff8bf7..bca6fbc072a1 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -154,6 +154,7 @@ /* urlUtils.js */ "urlResolve": false, "urlIsSameOrigin": false, + "urlIsAllowedOriginChecker": false, /* ng/controller.js */ "identifierForController": false, diff --git a/src/ng/http.js b/src/ng/http.js index 995a70ec8abb..6024c9c0cd1e 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -39,7 +39,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) { @@ -106,7 +106,7 @@ function $HttpParamSerializerJQLikeProvider() { * }); * ``` * - * */ + */ this.$get = function() { return function jQueryLikeParamSerializer(params) { if (!params) return ''; @@ -253,7 +253,7 @@ function isSuccess(status) { * * @description * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. - * */ + */ function $HttpProvider() { /** * @ngdoc property @@ -286,7 +286,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], @@ -331,7 +331,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; @@ -355,7 +355,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; @@ -376,9 +376,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). + * + *
+ * 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 + * + * ``` + * // 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', function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) { @@ -402,6 +442,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 @@ -778,25 +823,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)) * 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 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. + * + *
+ * **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 + * `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: @@ -1286,7 +1348,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 ee58b435f3e9..2d005b42d486 100644 --- a/src/ng/urlUtils.js +++ b/src/ng/urlUtils.js @@ -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 @@ -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)); + }; } diff --git a/test/.eslintrc.json b/test/.eslintrc.json index 2bf89b2bb07e..390b5eb59110 100644 --- a/test/.eslintrc.json +++ b/test/.eslintrc.json @@ -144,6 +144,7 @@ /* urlUtils.js */ "urlResolve": false, "urlIsSameOrigin": false, + "urlIsAllowedOriginChecker": false, /* karma */ "dump": false, diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index 709766122c14..269a718efaa3 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -9,22 +9,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) { @@ -35,7 +30,6 @@ describe('$http', function() { throw 'Unhandled exceptions trapped in $exceptionHandler!'; } - $rootScope.$digest(); $httpBackend.verifyNoOutstandingExpectation(); })); @@ -784,18 +778,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'); @@ -827,32 +809,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'; }}; @@ -902,20 +858,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(); - })); }); @@ -2128,6 +2070,139 @@ describe('$http', function() { }); + describe('XSRF', function() { + var $http; + var $httpBackend; + + beforeEach(module(function($httpProvider) { + $httpProvider.xsrfWhitelistedOrigins.push('https://whitelisted.example.com/'); + })); + + 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 currentUrl = 'https://example.com/path'; + var requestUrl = 'https://api.example.com/path'; + + mockedCookies['XSRF-TOKEN'] = 'secret'; + $httpBackend.expect('GET', requestUrl, null, checkHeaders).respond(null); + + $http.get(requestUrl); + + $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 requestUrl = 'https://whitelisted.example.com/path'; + + $browser.url(currentUrl); + + mockedCookies['XSRF-TOKEN'] = 'secret'; + $httpBackend.expect('GET', requestUrl, null, checkHeaders).respond(null); + + $http.get(requestUrl); + + $httpBackend.flush(); + }) + ); + }); + + it('should pass timeout, withCredentials and responseType', function() { var $httpBackend = jasmine.createSpy('$httpBackend'); @@ -2348,5 +2423,4 @@ describe('$http param serializers', function() { //a[]=b&a[]=c&d[0][e]=f&d[0][g]=h&d[]=i&d[2][j]=k }); }); - }); diff --git a/test/ng/urlUtilsSpec.js b/test/ng/urlUtilsSpec.js index cd2519d98452..07ecd7c0f7d6 100644 --- a/test/ng/urlUtilsSpec.js +++ b/test/ng/urlUtilsSpec.js @@ -3,11 +3,12 @@ describe('urlUtils', function() { describe('urlResolve', function() { it('should normalize a relative url', function() { - expect(urlResolve("foo").href).toMatch(/^https?:\/\/[^/]+\/foo$/); + expect(urlResolve('foo').href).toMatch(/^https?:\/\/[^/]+\/foo$/); }); + it('should parse relative URL into component pieces', function() { - var parsed = urlResolve("foo"); + var parsed = urlResolve('foo'); expect(parsed.href).toMatch(/https?:\/\//); expect(parsed.protocol).toMatch(/^https?/); expect(parsed.host).not.toBe(""); @@ -23,22 +24,110 @@ describe('urlUtils', function() { }); }); - describe('isSameOrigin', 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); - } - 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', 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); + } + + 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('urlIsAllowedOriginChecker', function() { + var origin = urlResolve(window.location.href); + var urlIsAllowedOrigin; + + beforeEach(function() { + urlIsAllowedOrigin = urlIsAllowedOriginChecker([ + '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); + }); }); });