diff --git a/browserid/app.js b/browserid/app.js index 62193ab387997950e3e757946f2debe417b7678e..968a262120bc9a79ab92f2a88c7be10135c74b56 100644 --- a/browserid/app.js +++ b/browserid/app.js @@ -7,14 +7,14 @@ try { fs.mkdirSync(VAR_DIR, 0755); } catch(e) { }; const url = require('url'), + crypto = require('crypto'), wsapi = require('./lib/wsapi.js'), httputils = require('./lib/httputils.js'), webfinger = require('./lib/webfinger.js'), sessions = require('cookie-sessions'), express = require('express'), secrets = require('./lib/secrets.js'), - db = require('./lib/db.js'), - csrf = require('express-csrf'); + db = require('./lib/db.js') // looks unused, see run.js // const STATIC_DIR = path.join(path.dirname(__dirname), "static"); @@ -42,8 +42,16 @@ function router(app) { // simple redirects (internal for now) app.get('/register_iframe', internal_redirector('/dialog/register_iframe.html')); + // return the CSRF token + // IMPORTANT: this should be safe because it's only readable by same-origin code + // but we must be careful that this is never a JSON structure that could be hijacked + // by a third party + app.get('/csrf', function(req, res) { + res.write(req.session.csrf); + res.end(); + }); + app.get('/', function(req,res) { - console.log("CSRF: " + req.session.csrf); res.render('index.ejs', {title: 'A Better Way to Sign In', fullpage: true}); }); @@ -64,7 +72,7 @@ function router(app) { }); app.get('/manage', function(req,res) { - res.render('manage.ejs', {title: 'My Account', fullpage: false}); + res.render('manage.ejs', {title: 'My Account', fullpage: false, csrf: req.session.csrf}); }); app.get('/tos', function(req, res) { @@ -121,6 +129,13 @@ exports.setup = function(server) { server.use(function(req, resp, next) { if (typeof req.session == 'undefined') req.session = {}; + + if (typeof req.session.csrf == 'undefined') { + // FIXME: using express-csrf's approach for generating randomness + // not awesome, but probably sufficient for now. + req.session.csrf = crypto.createHash('md5').update('' + new Date().getTime()).digest('hex'); + } + next(); }); @@ -138,13 +153,18 @@ exports.setup = function(server) { next(); }); - // setup CSRF protection - //server.use(csrf.check()); - - server.dynamicHelpers({ - csrf: csrf.token - }); - + // check CSRF token + server.use(function(req, resp, next) { + // only on POSTs + if (req.method == "POST") { + if (req.body.csrf != req.session.csrf) { + // error, problem with CSRF + throw new Error("CSRF violation - " + req.body.csrf + '/' + req.session.csrf); + } + } + + next(); + }); // add the actual URL handlers other than static router(server); } diff --git a/browserid/lib/wsapi.js b/browserid/lib/wsapi.js index 57bccb035e72245d1b78649cf006a3017d9f4587..9e7cfd4b349629730df5e8cde32d911284e70bdd 100644 --- a/browserid/lib/wsapi.js +++ b/browserid/lib/wsapi.js @@ -33,7 +33,10 @@ function checkParams(params) { } function isAuthed(req) { - return (req.session && typeof req.session.authenticatedUser === 'string'); + var result= (req.session && typeof req.session.authenticatedUser === 'string'); + if (!result && req.session) + console.log("AUTH" + req.session.authenticatedUser); + return result; } // turned this into a proper middleware @@ -153,20 +156,19 @@ function setup(app) { }); }); - // FIXME: need CSRF protection - app.get('/wsapi/add_email', checkAuthed, checkParams(["email", "pubkey", "site"]), function (req, resp) { + app.post('/wsapi/add_email', checkAuthed, checkParams(["email", "pubkey", "site"]), function (req, resp) { try { // upon success, stage_user returns a secret (that'll get baked into a url // and given to the user), on failure it throws - var secret = db.stageEmail(req.session.authenticatedUser, req.query.email, req.query.pubkey); + var secret = db.stageEmail(req.session.authenticatedUser, req.body.email, req.body.pubkey); // store the email being added in session data - req.session.pendingAddition = req.query.email; + req.session.pendingAddition = req.body.email; httputils.jsonResponse(resp, true); // let's now kick out a verification email! - email.sendVerificationEmail(req.query.email, req.query.site, secret); + email.sendVerificationEmail(req.body.email, req.body.site, secret); } catch(e) { // we should differentiate tween' 400 and 500 here. httputils.badRequest(resp, e.toString()); @@ -220,7 +222,7 @@ function setup(app) { }); app.post('/wsapi/sync_emails', checkAuthed, function(req,resp) { - var emails = req.body; + var emails = req.body.emails; db.getSyncResponse(req.session.authenticatedUser, emails, function(err, syncResponse) { if (err) httputils.serverError(resp, err); diff --git a/browserid/static/dialog/controllers/dialog_controller.js b/browserid/static/dialog/controllers/dialog_controller.js index fe771e70f1ad53f078c7d436bf645e6b1ea0c81f..d9e49b1a6357805d4573aa3732428ccc76d8f0ab 100644 --- a/browserid/static/dialog/controllers/dialog_controller.js +++ b/browserid/static/dialog/controllers/dialog_controller.js @@ -7,13 +7,27 @@ $.Controller("Dialog", {}, { init: function(el) { + // FIXME: there's a small chance the CSRF token doesn't come back in time. + this.csrf = null; + this._getCSRF(); + this.element.html("views/body.ejs", {}); this.element.show(); // keep track of where we are and what we do on success and error this.onsuccess = null; this.onerror = null; - var chan = setupChannel(this); + var chan = setupChannel(this); + }, + + _getCSRF: function(cb) { + // go get CSRF token + var self = this; + $.get('/csrf', {}, function(result) { + self.csrf = result; + if (cb) + cb(); + }); }, setupEnterKey: function() { @@ -97,9 +111,14 @@ $.Controller("Dialog", {}, { ); $.ajax({ - url: '/wsapi/add_email?email=' + encodeURIComponent(email) - + '&pubkey=' + encodeURIComponent(keypair.pub) - + '&site=' + encodeURIComponent(this.remoteOrigin.replace(/^(http|https):\/\//, '')), + type: 'POST', + url: '/wsapi/add_email', + data: { + email: email, + pubkey: keypair.pub, + site: this.remoteOrigin.replace(/^(http|https):\/\//, ''), + csrf: this.csrf + }, success: function() { // email successfully staged, now wait for email confirmation self.doConfirmEmail(email, keypair); @@ -116,8 +135,10 @@ $.Controller("Dialog", {}, { "#notme click": function(event) { clearEmails(); var self = this; - $.post("/wsapi/logout", function() { - self.doAuthenticate(); + $.post("/wsapi/logout", {csrf: this.csrf}, function() { + self._getCSRF(function() { + self.doAuthenticate(); + }); }); }, @@ -435,7 +456,8 @@ $.Controller("Dialog", {}, { $.ajax({ url: '/wsapi/sync_emails', type: "post", - data: JSON.stringify(issued_identities), + data: {'emails': JSON.stringify(issued_identities), + 'csrf': this.csrf}, success: function(resp, textStatus, jqXHR) { // first remove idenitites that the server doesn't know about if (resp.unknown_emails) { diff --git a/browserid/tests/forgotten-email-test.js b/browserid/tests/forgotten-email-test.js index 469978e75dd25862236dae90ff16a3de6e8afb5c..41375976f33d7eaa8e745702440a86d2329a5cbd 100755 --- a/browserid/tests/forgotten-email-test.js +++ b/browserid/tests/forgotten-email-test.js @@ -13,7 +13,7 @@ start_stop.addStartupBatches(suite); // ever time a new token is sent out, let's update the global // var 'token' var token = undefined; -interceptor.onEmail = function(newtok) { token = newtok; } +interceptor.onEmail = function(newtok) { token = newtok; }; // create a new account via the api with (first address) suite.addBatch({ @@ -54,7 +54,7 @@ suite.addBatch({ // add a new email address to the account (second address) suite.addBatch({ "add a new email address to our account": { - topic: wsapi.get('/wsapi/add_email', { + topic: wsapi.post('/wsapi/add_email', { email: 'second@fakeemail.com', pubkey: 'fakepubkey', site:'fakesite.com' @@ -159,9 +159,17 @@ suite.addBatch({ assert.strictEqual(JSON.parse(r.body), true); } }, + "sync emails": { + topic: wsapi.post('/wsapi/sync_emails', {'emails': '{}'}), + "should work" : function(r, err) { + var parsed_body = JSON.parse(r.body); + assert.equal(typeof parsed_body.unknown_emails, "object"); + assert.equal(typeof parsed_body.key_refresh, "object"); + } + }, "logout": { topic: wsapi.post('/wsapi/logout', {}), - "should work": function(r, err) { + "should work": function(r, err) { assert.strictEqual(JSON.parse(r.body), "ok"); } }, diff --git a/browserid/tests/lib/wsapi.js b/browserid/tests/lib/wsapi.js index 2a15fc7b99c3ce44d0fa29735627109c06ca0303..f6ba8b174b764d43025c29580e51924b55d0dfe4 100644 --- a/browserid/tests/lib/wsapi.js +++ b/browserid/tests/lib/wsapi.js @@ -47,14 +47,32 @@ exports.get = function (path, getArgs) { }; }; +// fetch the CSRF token for POSTs +var fetchCSRF = function(headers, cb) { + return http.get({ + host: '127.0.0.1', + port: '62700', + path: '/csrf', + headers: headers + }, function(res) { + var body = ""; + res.on('data', function(chunk) { body += chunk; }) + .on('end', function() { + cb(body); + }); + }).on('error', function (e) { + cb(); + }); +}; + + // FIXME: dedup code // A macro for wsapi requests +// add CSRF protection to this test exports.post = function (path, postArgs) { return function () { var cb = this.callback; - if (typeof postArgs === 'object') - body = querystring.stringify(postArgs); var headers = { 'content-type': 'application/x-www-form-urlencoded' @@ -67,34 +85,41 @@ exports.post = function (path, postArgs) { } } - var req = http.request({ - host: '127.0.0.1', - port: '62700', - path: path, - headers: headers, - method: "POST" - }, function(res) { - // see if there are any set-cookies that we should honor - if (res.headers['set-cookie']) { - res.headers['set-cookie'].forEach(function(cookie) { - var m = /^([^;]+)(?:;.*)$/.exec(cookie); - if (m) { - var x = m[1].split('='); - cookieJar[x[0]] = x[1]; - } - }); + fetchCSRF(headers, function(csrf) { + if (typeof postArgs === 'object') { + postArgs['csrf'] = csrf; + body = querystring.stringify(postArgs); } - var body = ''; - res.on('data', function(chunk) { body += chunk; }) - .on('end', function() { - cb({code: res.statusCode, headers: res.headers, body: body}); - }); - }).on('error', function (e) { - cb(); - }); - // send the POST - req.write(body); - req.end(); + var req = http.request({ + host: '127.0.0.1', + port: '62700', + path: path, + headers: headers, + method: "POST" + }, function(res) { + // see if there are any set-cookies that we should honor + if (res.headers['set-cookie']) { + res.headers['set-cookie'].forEach(function(cookie) { + var m = /^([^;]+)(?:;.*)$/.exec(cookie); + if (m) { + var x = m[1].split('='); + cookieJar[x[0]] = x[1]; + } + }); + } + var body = ''; + res.on('data', function(chunk) { body += chunk; }) + .on('end', function() { + cb({code: res.statusCode, headers: res.headers, body: body}); + }); + }).on('error', function (e) { + cb(); + }); + + // send the POST + req.write(body); + req.end(); + }); }; }; diff --git a/browserid/views/layout.ejs b/browserid/views/layout.ejs index 1ae2936f35a5f283b92d5e0d15f5e396055efc97..e396a3811932294a9f62ccd70b13d27f6d7c06bc 100644 --- a/browserid/views/layout.ejs +++ b/browserid/views/layout.ejs @@ -24,7 +24,7 @@ function display_saved_ids() $('#cancellink').click(function() { if (confirm('Are you sure you want to cancel your account?')) { - $.post("/wsapi/account_cancel", {}, function(result) { + $.post("/wsapi/account_cancel", {csrf: window.csrf}, function(result) { window.localStorage.emails = null; document.location="/"; }); @@ -58,7 +58,7 @@ function display_saved_ids() deauth.click(function() { var t = JSON.parse(window.localStorage.emails); // remove email from server - $.post("/wsapi/remove_email", {"email" : e}, function(response) { + $.post("/wsapi/remove_email", {"email" : e, "csrf": window.csrf}, function(response) { // we delete from store only once we got response delete t[e]; window.localStorage.emails = JSON.stringify(t); diff --git a/browserid/views/manage.ejs b/browserid/views/manage.ejs index f48ac42d97b917162a7bc7338a9795d4f5e8bcbf..8ca5046c78132eefc349b7b3878cd9c45a6362d2 100644 --- a/browserid/views/manage.ejs +++ b/browserid/views/manage.ejs @@ -1,3 +1,6 @@ + <script> +window.csrf = "<%= csrf %>"; + </script> <div class="why"> <p> Manage your email addresses in BrowserID.