reload: Fix passing data to next browser session.

Apparently, Django's CSRF protection mechanism changed at some point,
and now we get a different CSRF token every time the webapp is loaded.
This, in turn, caused our reload logic to avoid losing state to be
completely ineffective, since the CSRF check in reload.initialize
always failed.

We fix this in a secure fashion by passing the reload instructions
from the browser to its reloaded self via localstorage, keyed by a
randomly generated token.  The token randomization is primarily
relevant for handling several Zulip tabs in the same browser, but also
servers to make it very difficult for an attacker to ever trigger this
code path by redirecting a browser to `/#reload` URLs.

Fixes #3411.
Fixes #3687.
This commit is contained in:
Tim Abbott 2017-03-22 22:21:13 -07:00
parent 64acf84ab1
commit 04db0b5df0
1 changed files with 29 additions and 9 deletions

View File

@ -69,7 +69,19 @@ function preserve_state(send_after_reload, save_pointer, save_narrow, save_compo
} }
url += "+oldhash=" + encodeURIComponent(oldhash); url += "+oldhash=" + encodeURIComponent(oldhash);
window.location.replace(url); // To protect the browser against CSRF type attacks, the reload
// logic uses a random token (to distinct this browser from
// others) which is passed via the URL to the browser (post
// reloading). The token is a key into local storage, where we
// marshall and store the URL.
//
// TODO: Remove the now-unnecessary URL-encoding logic above and
// just pass the actual data structures through local storage.
var token = util.random_int(0, 1024*1024*1024*1024);
var ls = localstorage();
ls.set("reload:" + token, url);
window.location.replace("#reload:" + token);
} }
@ -77,10 +89,24 @@ function preserve_state(send_after_reload, save_pointer, save_narrow, save_compo
// done before the first call to get_events // done before the first call to get_events
exports.initialize = function reload__initialize() { exports.initialize = function reload__initialize() {
var location = window.location.toString(); var location = window.location.toString();
var fragment = location.substring(location.indexOf('#') + 1); var hash_fragment = location.substring(location.indexOf('#') + 1);
if (fragment.search("reload:") !== 0) {
// hash_fragment should be e.g. `reload:12345123412312`
if (hash_fragment.search("reload:") !== 0) {
return; return;
} }
// Using the token, recover the saved pre-reload data from local
// storage. Afterwards, we clear the reload entry from local
// storage to avoid a local storage space leak.
var ls = localstorage();
var fragment = ls.get(hash_fragment);
if (fragment === undefined) {
blueslip.error("Invalid hash change reload token");
return;
}
ls.remove(hash_fragment);
fragment = fragment.replace(/^reload:/, ""); fragment = fragment.replace(/^reload:/, "");
var keyvals = fragment.split("+"); var keyvals = fragment.split("+");
var vars = {}; var vars = {};
@ -89,12 +115,6 @@ exports.initialize = function reload__initialize() {
vars[pair[0]] = decodeURIComponent(pair[1]); vars[pair[0]] = decodeURIComponent(pair[1]);
}); });
// Prevent random people on the Internet from constructing links
// that make you send a message.
if (vars.csrf_token !== csrf_token) {
return;
}
if (vars.msg !== undefined) { if (vars.msg !== undefined) {
var send_now = parseInt(vars.send_after_reload, 10); var send_now = parseInt(vars.send_after_reload, 10);