Skip to content

Commit 156e5cb

Browse files
committed
fix($http) Only set X-XSFR-TOKEN header for same-domain request.
fixes angular#1096
1 parent c6b4ab3 commit 156e5cb

File tree

3 files changed

+70
-2
lines changed

3 files changed

+70
-2
lines changed

src/ng/http.js

+37-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,39 @@ function parseHeaders(headers) {
2828
return parsed;
2929
}
3030

31+
/**
32+
* Parse a request and location URL and determine whether this is a same-domain request.
33+
*
34+
* @param {string} requestUrl The url of the request.
35+
* @param {string} locationUrl The current browser location url.
36+
* @returns {boolean} Whether the request is for the same domain.
37+
*/
38+
function isSameDomain(requestUrl, locationUrl) {
39+
var match = XML_REQUEST_URL_MATCH.exec(requestUrl);
40+
// if requestUrl is relative, the regex does not match.
41+
if (match == null) return true;
42+
43+
var domain1 = {
44+
protocol: match[2],
45+
host: match[4],
46+
port: int(match[6]) || DEFAULT_PORTS[match[2]] || null,
47+
// IE8 sets unmatched groups to '' instead of undefined.
48+
relativeProtocol: match[2] === undefined || match[2] === ''
49+
};
50+
51+
match = URL_MATCH.exec(locationUrl);
52+
var domain2 = {
53+
protocol: match[1],
54+
host: match[3],
55+
port: int(match[5]) || DEFAULT_PORTS[match[1]] || null
56+
};
57+
58+
return (domain1.protocol == domain2.protocol || domain1.relativeProtocol) &&
59+
domain1.host == domain2.host &&
60+
(domain1.port == domain2.port || (domain1.relativeProtocol &&
61+
domain2.port == DEFAULT_PORTS[domain2.protocol]));
62+
}
63+
3164

3265
/**
3366
* Returns a function that provides access to parsed headers.
@@ -347,7 +380,7 @@ function $HttpProvider() {
347380
* to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie
348381
* called `XSRF-TOKEN` and sets it as the HTTP header `X-XSRF-TOKEN`. Since only JavaScript that
349382
* runs on your domain could read the cookie, your server can be assured that the XHR came from
350-
* JavaScript running on your domain.
383+
* JavaScript running on your domain. The header will not be set for cross-domain requests.
351384
*
352385
* To take advantage of this, your server needs to set a token in a JavaScript readable session
353386
* cookie called `XSRF-TOKEN` on first HTTP GET request. On subsequent non-GET requests the
@@ -478,7 +511,9 @@ function $HttpProvider() {
478511
var reqTransformFn = config.transformRequest || defaults.transformRequest,
479512
respTransformFn = config.transformResponse || defaults.transformResponse,
480513
defHeaders = defaults.headers,
481-
reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']},
514+
xsrfToken = isSameDomain(config.url, $browser.url()) ?
515+
$browser.cookies()['XSRF-TOKEN'] : undefined,
516+
reqHeaders = extend({'X-XSRF-TOKEN': xsrfToken},
482517
defHeaders.common, defHeaders[lowercase(config.method)], config.headers),
483518
reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn),
484519
promise;

src/ng/location.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
var URL_MATCH = /^([^:]+):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/,
4+
XML_REQUEST_URL_MATCH = /^(([^:]+):)?\/\/(\w+:{0,1}\w*@)?([\w\.-]*)?(:([0-9]+))?(.*)$/,
45
PATH_MATCH = /^([^\?#]*)?(\?([^#]*))?(#(.*))?$/,
56
HASH_MATCH = PATH_MATCH,
67
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};

test/ng/httpSpec.js

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

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

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

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

0 commit comments

Comments
 (0)