Skip to content

Commit 96561a2

Browse files
committed
Properly check if the request is cross domain
Fix CVE-2015-1840
1 parent 519aad9 commit 96561a2

File tree

5 files changed

+52
-53
lines changed

5 files changed

+52
-53
lines changed

src/rails.js

+25-6
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,14 @@
8686

8787
// Default way to get an element's href. May be overridden at $.rails.href.
8888
href: function(element) {
89-
return element.attr('href');
89+
return element[0].href;
9090
},
9191

9292
// Submits "remote" forms and links with ajax
9393
handleRemote: function(element) {
94-
var method, url, data, elCrossDomain, crossDomain, withCredentials, dataType, options;
94+
var method, url, data, withCredentials, dataType, options;
9595

9696
if (rails.fire(element, 'ajax:before')) {
97-
elCrossDomain = element.data('cross-domain');
98-
crossDomain = elCrossDomain === undefined ? null : elCrossDomain;
9997
withCredentials = element.data('with-credentials') || null;
10098
dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType);
10199

@@ -147,7 +145,7 @@
147145
error: function(xhr, status, error) {
148146
element.trigger('ajax:error', [xhr, status, error]);
149147
},
150-
crossDomain: crossDomain
148+
crossDomain: rails.isCrossDomain(url)
151149
};
152150

153151
// There is no withCredentials for IE6-8 when
@@ -167,6 +165,27 @@
167165
}
168166
},
169167

168+
// Determines if the request is a cross domain request.
169+
isCrossDomain: function(url) {
170+
var originAnchor = document.createElement("a");
171+
originAnchor.href = location.href;
172+
var urlAnchor = document.createElement("a");
173+
174+
try {
175+
urlAnchor.href = url;
176+
// This is a workaround to a IE bug.
177+
urlAnchor.href = urlAnchor.href;
178+
179+
// Make sure that the browser parses the URL and that the protocols and hosts match.
180+
return !urlAnchor.protocol || !urlAnchor.host ||
181+
(originAnchor.protocol + "//" + originAnchor.host !==
182+
urlAnchor.protocol + "//" + urlAnchor.host);
183+
} catch (e) {
184+
// If there is an error parsing the URL, assume it is crossDomain.
185+
return true;
186+
}
187+
},
188+
170189
// Handles "data-method" on links such as:
171190
// <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
172191
handleMethod: function(link) {
@@ -178,7 +197,7 @@
178197
form = $('<form method="post" action="' + href + '"></form>'),
179198
metadataInput = '<input name="_method" value="' + method + '" type="hidden" />';
180199

181-
if (csrfParam !== undefined && csrfToken !== undefined) {
200+
if (csrfParam !== undefined && csrfToken !== undefined && !rails.isCrossDomain(href)) {
182201
metadataInput += '<input name="' + csrfParam + '" value="' + csrfToken + '" type="hidden" />';
183202
}
184203

test/public/test/call-remote-callbacks.js

-14
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,6 @@ asyncTest('modifying data("type") with "ajax:before" requests new dataType in re
6464
});
6565
});
6666

67-
asyncTest('setting data("cross-domain",true) with "ajax:before" uses new setting in request', 2, function(){
68-
$('form[data-remote]').data('cross-domain',false)
69-
.bind('ajax:before', function() {
70-
var form = $(this);
71-
form.data('cross-domain',true);
72-
});
73-
74-
submit(function(form) {
75-
form.bind('ajax:beforeSend', function(e, xhr, settings) {
76-
equal(settings.crossDomain, true, 'setting modified in ajax:before should have forced cross-domain request');
77-
});
78-
});
79-
});
80-
8167
asyncTest('setting data("with-credentials",true) with "ajax:before" uses new setting in request', 2, function(){
8268
$('form[data-remote]').data('with-credentials',false)
8369
.bind('ajax:before', function() {

test/public/test/call-remote.js

-32
Original file line numberDiff line numberDiff line change
@@ -122,22 +122,6 @@ asyncTest('sends CSRF token in custom header', 1, function() {
122122
});
123123
});
124124

125-
asyncTest('does not send CSRF token in custom header if crossDomain', 1, function() {
126-
buildForm({ 'data-cross-domain': 'true' });
127-
$('#qunit-fixture').append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae" />');
128-
129-
// Manually set request header to be XHR, since setting crossDomain: true in .ajax()
130-
// causes jQuery to skip setting the request header, to prevent our test/server.rb from
131-
// raising an an error (when request.xhr? is false).
132-
$('#qunit-fixture').find('form').bind('ajax:beforeSend', function(e, xhr) {
133-
xhr.setRequestHeader('X-Requested-With', "XMLHttpRequest");
134-
});
135-
136-
submit(function(e, data, status, xhr) {
137-
equal(data.HTTP_X_CSRF_TOKEN, undefined, 'X-CSRF-Token header should NOT be sent');
138-
});
139-
});
140-
141125
asyncTest('intelligently guesses crossDomain behavior when target URL is a different domain', 1, function(e, xhr) {
142126

143127
// Don't set data-cross-domain here, just set action to be a different domain than localhost
@@ -156,20 +140,4 @@ asyncTest('intelligently guesses crossDomain behavior when target URL is a diffe
156140

157141
setTimeout(function() { start(); }, 13);
158142
});
159-
160-
asyncTest('does not set crossDomain if explicitly set to false on element', 1, function() {
161-
buildForm({ action: 'http://www.alfajango.com', 'data-cross-domain': false });
162-
$('#qunit-fixture').append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae" />');
163-
164-
$('#qunit-fixture').find('form')
165-
.bind('ajax:beforeSend', function(e, xhr, settings) {
166-
equal(settings.crossDomain, false, 'crossDomain should be set to false');
167-
// prevent request from actually getting sent off-domain
168-
return false;
169-
})
170-
.trigger('submit');
171-
172-
setTimeout(function() { start(); }, 13);
173-
});
174-
175143
})();

test/public/test/data-method.js

+26
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,30 @@ asyncTest('link "target" should be carried over to generated form', 1, function(
4646
});
4747
});
4848

49+
asyncTest('link with "data-method" and cross origin', 1, function() {
50+
var data = {};
51+
52+
$('#qunit-fixture')
53+
.append('<meta name="csrf-param" content="authenticity_token"/>')
54+
.append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae"/>');
55+
56+
$(document).on('submit', 'form', function(e) {
57+
$(e.currentTarget).serializeArray().map(function(item) {
58+
data[item.name] = item.value;
59+
});
60+
61+
return false;
62+
});
63+
64+
var link = $('#qunit-fixture').find('a');
65+
66+
link.attr('href', 'http://www.alfajango.com');
67+
68+
link.trigger('click');
69+
70+
start();
71+
72+
notEqual(data.authenticity_token, 'cf50faa3fe97702ca1ae');
73+
});
74+
4975
})();

test/public/test/override.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ asyncTest("the getter for an element's href is overridable", 1, function() {
3232

3333
asyncTest("the getter for an element's href works normally if not overridden", 1, function() {
3434
$.rails.ajax = function(options) {
35-
equal('/real/href', options.url);
35+
equal(location.protocol + '//' + location.host + '/real/href', options.url);
3636
}
3737
$.rails.handleRemote($('#qunit-fixture').find('a'));
3838
start();

0 commit comments

Comments
 (0)