Skip to content

Commit 633e82a

Browse files
committed
fix(connector): add CSRF token validation and refresh flow
- issue CSRF token on open init response - send token via custom request header - validate token for protected write commands - refresh token TTL on info reload and valid protected requests - return csrfReload marker on 403 to trigger background token refresh - suppress duplicate CSRF refresh requests on the client
1 parent 63369c6 commit 633e82a

2 files changed

Lines changed: 377 additions & 2 deletions

File tree

js/elFinder.js

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,13 @@ var elFinder = function(elm, opts, bootCallback) {
643643
*/
644644
currentOpenCmd = null,
645645

646+
/**
647+
* Current CSRF refresh request instance
648+
*
649+
* @type Object
650+
*/
651+
currentCsrfRefresh = null,
652+
646653
/**
647654
* Exec shortcut
648655
*
@@ -1410,6 +1417,97 @@ var elFinder = function(elm, opts, bootCallback) {
14101417
*/
14111418
this.customHeaders = $.isPlainObject(this.options.customHeaders) ? this.options.customHeaders : {};
14121419

1420+
/**
1421+
* CSRF header name for connector requests
1422+
*
1423+
* @type String
1424+
*/
1425+
this.csrfHeaderName = 'X-elFinder-CSRF';
1426+
1427+
/**
1428+
* JSON response key for CSRF token
1429+
*
1430+
* @type String
1431+
*/
1432+
this.csrfResponseKey = 'csrf';
1433+
1434+
/**
1435+
* JSON response key that marks a CSRF-triggered reload request
1436+
*
1437+
* @type String
1438+
*/
1439+
this.csrfReloadKey = 'csrfReload';
1440+
1441+
/**
1442+
* Store or clear current CSRF token header
1443+
*
1444+
* @param {String} token
1445+
* @return void
1446+
*/
1447+
this.setCsrfToken = function(token) {
1448+
if (typeof token === 'string' && token) {
1449+
self.customHeaders[self.csrfHeaderName] = token;
1450+
} else {
1451+
delete self.customHeaders[self.csrfHeaderName];
1452+
}
1453+
};
1454+
1455+
/**
1456+
* Determine whether the response represents a CSRF refreshable failure
1457+
*
1458+
* @param {Object} xhr
1459+
* @param {Object} response
1460+
* @return {Boolean}
1461+
*/
1462+
this.isCsrfReloadResponse = function(xhr, response) {
1463+
return !!(xhr
1464+
&& xhr.status === 403
1465+
&& !xhr._csrfRefresh
1466+
&& response
1467+
&& response[self.csrfReloadKey]);
1468+
};
1469+
1470+
/**
1471+
* Refresh CSRF token via open&init=1 without replaying the failed command
1472+
*
1473+
* @return {jQuery.Deferred}
1474+
*/
1475+
this.refreshCsrfToken = function() {
1476+
var cwdFile = self.cwd(),
1477+
target = (cwdFile && cwdFile.hash) || self.lastDir('') || self.startDir();
1478+
1479+
if (currentCsrfRefresh && currentCsrfRefresh.state() === 'pending') {
1480+
return currentCsrfRefresh;
1481+
}
1482+
1483+
currentCsrfRefresh = self.request({
1484+
data : {cmd : 'open', target : target, init : 1, tree : 1},
1485+
preventDefault : true,
1486+
preventFail : true,
1487+
_csrfRefresh : true
1488+
}).always(function() {
1489+
currentCsrfRefresh = null;
1490+
});
1491+
1492+
return currentCsrfRefresh;
1493+
};
1494+
1495+
/**
1496+
* Start background CSRF refresh if current failure is refreshable
1497+
*
1498+
* @param {Object} xhr
1499+
* @param {Object} response
1500+
* @return {Boolean}
1501+
*/
1502+
this.handleCsrfReload = function(xhr, response) {
1503+
if (!self.isCsrfReloadResponse(xhr, response)) {
1504+
return false;
1505+
}
1506+
1507+
self.refreshCsrfToken();
1508+
return true;
1509+
};
1510+
14131511
/**
14141512
* Any custom xhrFields to send across every ajax request
14151513
*
@@ -2443,6 +2541,7 @@ var elFinder = function(elm, opts, bootCallback) {
24432541
// check responseText, Is that JSON?
24442542
try {
24452543
data = JSON.parse(xhr.responseText);
2544+
xhr._elfinderResponse = data;
24462545
if (data && data.error) {
24472546
error = data.error;
24482547
}
@@ -2496,7 +2595,13 @@ var elFinder = function(elm, opts, bootCallback) {
24962595
return dfrd.reject({error :['errResponse', 'errDataEmpty']}, xhr, response);
24972596
} else if (!$.isPlainObject(response)) {
24982597
return dfrd.reject({error :['errResponse', 'errDataNotJSON']}, xhr, response);
2499-
} else if (response.error) {
2598+
}
2599+
2600+
if (isOpen && !!data.init && Object.prototype.hasOwnProperty.call(response, self.csrfResponseKey)) {
2601+
self.setCsrfToken(response[self.csrfResponseKey]);
2602+
}
2603+
2604+
if (response.error) {
25002605
if (isOpen) {
25012606
// check leafRoots
25022607
$.each(self.leafRoots, function(phash, roots) {
@@ -2735,6 +2840,12 @@ var elFinder = function(elm, opts, bootCallback) {
27352840
if (error) {
27362841
error.error = '';
27372842
}
2843+
} else if (self.handleCsrfReload(xhr, xhr._elfinderResponse)) {
2844+
deffail = false;
2845+
syncOnFail = false;
2846+
if (error) {
2847+
error.error = '';
2848+
}
27382849
}
27392850
// abort xhr
27402851
xhrAbort();
@@ -2812,6 +2923,7 @@ var elFinder = function(elm, opts, bootCallback) {
28122923
requestQueue.shift()();
28132924
}
28142925
}).fail(error).done(success);
2926+
xhr._csrfRefresh = !!opts._csrfRefresh;
28152927

28162928
if (self.api >= 2.1029) {
28172929
xhr._requestId = reqId;
@@ -6846,6 +6958,8 @@ elFinder.prototype = {
68466958
self.trigger('requestError', errData);
68476959
if (errData._getEvent && errData._getEvent().isDefaultPrevented()) {
68486960
res.error = '';
6961+
} else if (self.handleCsrfReload(xhr, res)) {
6962+
res.error = '';
68496963
}
68506964
if (res._chunkfailure || res._multiupload) {
68516965
abort = true;

0 commit comments

Comments
 (0)