Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit fce100a

Browse files
rkirovIgorMinar
authored andcommitted
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
1 parent 3a75b11 commit fce100a

File tree

2 files changed

+73
-2
lines changed

2 files changed

+73
-2
lines changed

src/ng/http.js

+41-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,43 @@ function parseHeaders(headers) {
2929
}
3030

3131

32+
var IS_SAME_DOMAIN_URL_MATCH = /^(([^:]+):)?\/\/(\w+:{0,1}\w*@)?([\w\.-]*)?(:([0-9]+))?(.*)$/;
33+
34+
35+
/**
36+
* Parse a request and location URL and determine whether this is a same-domain request.
37+
*
38+
* @param {string} requestUrl The url of the request.
39+
* @param {string} locationUrl The current browser location url.
40+
* @returns {boolean} Whether the request is for the same domain.
41+
*/
42+
function isSameDomain(requestUrl, locationUrl) {
43+
var match = IS_SAME_DOMAIN_URL_MATCH.exec(requestUrl);
44+
// if requestUrl is relative, the regex does not match.
45+
if (match == null) return true;
46+
47+
var domain1 = {
48+
protocol: match[2],
49+
host: match[4],
50+
port: int(match[6]) || DEFAULT_PORTS[match[2]] || null,
51+
// IE8 sets unmatched groups to '' instead of undefined.
52+
relativeProtocol: match[2] === undefined || match[2] === ''
53+
};
54+
55+
match = URL_MATCH.exec(locationUrl);
56+
var domain2 = {
57+
protocol: match[1],
58+
host: match[3],
59+
port: int(match[5]) || DEFAULT_PORTS[match[1]] || null
60+
};
61+
62+
return (domain1.protocol == domain2.protocol || domain1.relativeProtocol) &&
63+
domain1.host == domain2.host &&
64+
(domain1.port == domain2.port || (domain1.relativeProtocol &&
65+
domain2.port == DEFAULT_PORTS[domain2.protocol]));
66+
}
67+
68+
3269
/**
3370
* Returns a function that provides access to parsed headers.
3471
*
@@ -345,7 +382,7 @@ function $HttpProvider() {
345382
* to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie
346383
* called `XSRF-TOKEN` and sets it as the HTTP header `X-XSRF-TOKEN`. Since only JavaScript that
347384
* runs on your domain could read the cookie, your server can be assured that the XHR came from
348-
* JavaScript running on your domain.
385+
* JavaScript running on your domain. The header will not be set for cross-domain requests.
349386
*
350387
* To take advantage of this, your server needs to set a token in a JavaScript readable session
351388
* cookie called `XSRF-TOKEN` on first HTTP GET request. On subsequent non-GET requests the
@@ -476,7 +513,9 @@ function $HttpProvider() {
476513
var reqTransformFn = config.transformRequest || defaults.transformRequest,
477514
respTransformFn = config.transformResponse || defaults.transformResponse,
478515
defHeaders = defaults.headers,
479-
reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']},
516+
xsrfToken = isSameDomain(config.url, $browser.url()) ?
517+
$browser.cookies()['XSRF-TOKEN'] : undefined,
518+
reqHeaders = extend({'X-XSRF-TOKEN': xsrfToken},
480519
defHeaders.common, defHeaders[lowercase(config.method)], config.headers),
481520
reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn),
482521
promise;

test/ng/httpSpec.js

+32
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,17 @@ describe('$http', function() {
430430
$httpBackend.flush();
431431
});
432432

433+
it('should not set XSRF cookie for cross-domain requests', inject(function($browser) {
434+
$browser.cookies('XSRF-TOKEN', 'secret');
435+
$browser.url('http://host.com/base');
436+
$httpBackend.expect('GET', 'http://www.test.com/url', undefined, function(headers) {
437+
return headers['X-XSRF-TOKEN'] === undefined;
438+
}).respond('');
439+
440+
$http({url: 'http://www.test.com/url', method: 'GET', headers: {}});
441+
$httpBackend.flush();
442+
}));
443+
433444

434445
it('should not send Content-Type header if request data/body is undefined', function() {
435446
$httpBackend.expect('POST', '/url', undefined, function(headers) {
@@ -1005,4 +1016,25 @@ describe('$http', function() {
10051016

10061017
$httpBackend.verifyNoOutstandingExpectation = noop;
10071018
});
1019+
1020+
describe('isSameDomain', function() {
1021+
it('should support various combinations of urls', function() {
1022+
expect(isSameDomain('path/morepath',
1023+
'http://www.adomain.com')).toBe(true);
1024+
expect(isSameDomain('http://www.adomain.com/path',
1025+
'http://www.adomain.com')).toBe(true);
1026+
expect(isSameDomain('//www.adomain.com/path',
1027+
'http://www.adomain.com')).toBe(true);
1028+
expect(isSameDomain('//www.adomain.com/path',
1029+
'https://www.adomain.com')).toBe(true);
1030+
expect(isSameDomain('//www.adomain.com/path',
1031+
'http://www.adomain.com:1234')).toBe(false);
1032+
expect(isSameDomain('https://www.adomain.com/path',
1033+
'http://www.adomain.com')).toBe(false);
1034+
expect(isSameDomain('http://www.adomain.com:1234/path',
1035+
'http://www.adomain.com')).toBe(false);
1036+
expect(isSameDomain('http://www.anotherdomain.com/path',
1037+
'http://www.adomain.com')).toBe(false);
1038+
});
1039+
});
10081040
});

0 commit comments

Comments
 (0)