From fce100a46c5681562253c3a856d67bbd35fbc2f2 Mon Sep 17 00:00:00 2001 From: Rado Kirov Date: Fri, 28 Sep 2012 15:43:01 -0700 Subject: [PATCH] fix($http): only set X-XSFR-TOKEN header for same-domain request This is needed to prevent CORS preflight checks. The XSFR token is quite useless for CORS requests anyway. BREAKING CHANGE: X-XSFR-TOKEN is no longer send for cross domain requests. This shouldn't affect any known production service. Closes #1096 --- src/ng/http.js | 43 +++++++++++++++++++++++++++++++++++++++++-- test/ng/httpSpec.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/ng/http.js b/src/ng/http.js index 19e50dc18105..1f1f5e44f420 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -29,6 +29,43 @@ function parseHeaders(headers) { } +var IS_SAME_DOMAIN_URL_MATCH = /^(([^:]+):)?\/\/(\w+:{0,1}\w*@)?([\w\.-]*)?(:([0-9]+))?(.*)$/; + + +/** + * Parse a request and location URL and determine whether this is a same-domain request. + * + * @param {string} requestUrl The url of the request. + * @param {string} locationUrl The current browser location url. + * @returns {boolean} Whether the request is for the same domain. + */ +function isSameDomain(requestUrl, locationUrl) { + var match = IS_SAME_DOMAIN_URL_MATCH.exec(requestUrl); + // if requestUrl is relative, the regex does not match. + if (match == null) return true; + + var domain1 = { + protocol: match[2], + host: match[4], + port: int(match[6]) || DEFAULT_PORTS[match[2]] || null, + // IE8 sets unmatched groups to '' instead of undefined. + relativeProtocol: match[2] === undefined || match[2] === '' + }; + + match = URL_MATCH.exec(locationUrl); + var domain2 = { + protocol: match[1], + host: match[3], + port: int(match[5]) || DEFAULT_PORTS[match[1]] || null + }; + + return (domain1.protocol == domain2.protocol || domain1.relativeProtocol) && + domain1.host == domain2.host && + (domain1.port == domain2.port || (domain1.relativeProtocol && + domain2.port == DEFAULT_PORTS[domain2.protocol])); +} + + /** * Returns a function that provides access to parsed headers. * @@ -345,7 +382,7 @@ function $HttpProvider() { * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie * called `XSRF-TOKEN` and sets it as the 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. + * JavaScript running on your domain. The header will not be set for cross-domain requests. * * To take advantage of this, your server needs to set a token in a JavaScript readable session * cookie called `XSRF-TOKEN` on first HTTP GET request. On subsequent non-GET requests the @@ -476,7 +513,9 @@ function $HttpProvider() { var reqTransformFn = config.transformRequest || defaults.transformRequest, respTransformFn = config.transformResponse || defaults.transformResponse, defHeaders = defaults.headers, - reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, + xsrfToken = isSameDomain(config.url, $browser.url()) ? + $browser.cookies()['XSRF-TOKEN'] : undefined, + reqHeaders = extend({'X-XSRF-TOKEN': xsrfToken}, defHeaders.common, defHeaders[lowercase(config.method)], config.headers), reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn), promise; diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index 5049a2180c3c..1473ab1ccc3f 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -430,6 +430,17 @@ describe('$http', function() { $httpBackend.flush(); }); + it('should not set XSRF cookie for cross-domain requests', inject(function($browser) { + $browser.cookies('XSRF-TOKEN', 'secret'); + $browser.url('http://host.com/base'); + $httpBackend.expect('GET', 'http://www.test.com/url', undefined, function(headers) { + return headers['X-XSRF-TOKEN'] === undefined; + }).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) { @@ -1005,4 +1016,25 @@ describe('$http', function() { $httpBackend.verifyNoOutstandingExpectation = noop; }); + + describe('isSameDomain', function() { + it('should support various combinations of urls', function() { + expect(isSameDomain('path/morepath', + 'http://www.adomain.com')).toBe(true); + expect(isSameDomain('http://www.adomain.com/path', + 'http://www.adomain.com')).toBe(true); + expect(isSameDomain('//www.adomain.com/path', + 'http://www.adomain.com')).toBe(true); + expect(isSameDomain('//www.adomain.com/path', + 'https://www.adomain.com')).toBe(true); + expect(isSameDomain('//www.adomain.com/path', + 'http://www.adomain.com:1234')).toBe(false); + expect(isSameDomain('https://www.adomain.com/path', + 'http://www.adomain.com')).toBe(false); + expect(isSameDomain('http://www.adomain.com:1234/path', + 'http://www.adomain.com')).toBe(false); + expect(isSameDomain('http://www.anotherdomain.com/path', + 'http://www.adomain.com')).toBe(false); + }); + }); });