From 511b56aec5f96c6ad2be73c3d7adf3405dc0683c Mon Sep 17 00:00:00 2001
From: Lloyd Hilaiel <lloyd@hilaiel.com>
Date: Tue, 11 Oct 2011 14:01:36 -0600
Subject: [PATCH] WSAPI CHANGES: All server responses are now objects, makes
 some funky browsers happy and prevents certain attacks.  closes #217 closes
 #325

  * /wsapi/have_email now returns { email_known: <boolean> }
  * /wsapi/stage_user now returns { success: <boolean> }
  * /wsapi/user_creation_status now returns { status: <string> }
  * /wsapi/complete_user_creation now returns { status: <boolean> }
  * /wsapi/stage_email now returns { success: <boolean> }
  * /wsapi/email_addition_status now returns { success: <boolean> }
  * /wsapi/complete_email_addition now returns { success: <boolean> }
  * /wsapi/authenticate_user now returns { success: <boolean> }
  * /wsapi/remove_email now returns { success: <boolean> }
  * /wsapi/account_cancel now returns { success: <boolean> }
  * /wsapi/logout now returns { success: <boolean> }

Finally, introduced middleware to ensure that resp.json() is not called with anything other than an object.
---
 browserid/app.js                              | 13 +++++
 browserid/lib/wsapi.js                        | 46 +++++++++---------
 browserid/static/dialog/resources/network.js  | 33 ++++++++-----
 .../dialog/test/qunit/network_unit_test.js    | 48 +++++++++----------
 browserid/tests/cert-emails-test.js           |  2 +-
 browserid/tests/forgotten-email-test.js       | 30 ++++++------
 browserid/tests/list-emails-wsapi-test.js     |  4 +-
 .../tests/registration-status-wsapi-test.js   | 14 +++---
 8 files changed, 105 insertions(+), 85 deletions(-)

diff --git a/browserid/app.js b/browserid/app.js
index 946105636..a5abe7248 100644
--- a/browserid/app.js
+++ b/browserid/app.js
@@ -219,6 +219,19 @@ exports.setup = function(server) {
     }
   });
 
+  // verify all JSON responses are objects - prevents regression on issue #217
+  server.use(function(req, resp, next) {
+    var realRespJSON = resp.json;
+    resp.json = function(obj) {
+      if (!obj || typeof obj !== 'object') {
+        logger.error("INTERNAL ERROR!  *all* json responses must be objects");
+        throw "internal error";
+      }
+      realRespJSON.call(resp, obj);
+    };
+    return next();
+  });
+
   server.use(express.bodyParser());
 
   // Check CSRF token early.  POST requests are only allowed to
diff --git a/browserid/lib/wsapi.js b/browserid/lib/wsapi.js
index 6585caff0..2a84dc1c2 100644
--- a/browserid/lib/wsapi.js
+++ b/browserid/lib/wsapi.js
@@ -164,7 +164,7 @@ function setup(app) {
     // get inputs from get data!
     var email = url.parse(req.url, true).query['email'];
     db.emailKnown(email, function(known) {
-      resp.json(known);
+      resp.json({ email_known: known });
     });
   });
 
@@ -189,7 +189,7 @@ function setup(app) {
         // status of the email verification.
         req.session.pendingCreation = secret;
 
-        resp.json(true);
+        resp.json({ success: true });
 
         // let's now kick out a verification email!
         email.sendNewUserEmail(req.body.email, req.body.site, secret);
@@ -210,24 +210,24 @@ function setup(app) {
 
     // if the user is authenticated as the user in question, we're done
     if (isAuthed(req) && req.session.authenticatedUser === email) {
-      return resp.json('complete');
+      return resp.json({ status: 'complete' });
     }
     // if the user isn't authenticated and there's no pendingCreation token,
     // then they must authenticate
     else if (!req.session.pendingCreation) {
-      return resp.json('mustAuth');
+      return resp.json({ status: 'mustAuth' });
     }
 
     // if the secret is still in the database, it hasn't yet been verified and
     // verification is still pending
     db.emailForVerificationSecret(req.session.pendingCreation, function (email) {
-      if (email) return resp.json('pending');
+      if (email) return resp.json({ status: 'pending' });
       // if the secret isn't known, and we're not authenticated, then the user must authenticate
       // (maybe they verified the URL on a different browser, or maybe they canceled the account
       // creation)
       else {
         delete req.session.pendingCreation;
-        resp.json('mustAuth');
+        resp.json({ status: 'mustAuth' });
       }
     });
   });
@@ -247,30 +247,30 @@ function setup(app) {
     // bcrypting the password (which is expensive), to prevent a possible
     // DoS attack.
     db.emailForVerificationSecret(req.body.token, function(email) {
-      if (!email) return resp.json(false);
+      if (!email) return resp.json({ success: false} );
 
       // now bcrypt the password
       bcrypt.gen_salt(10, function (err, salt) {
         if (err) {
           logger.error("error generating salt with bcrypt: " + err);
-          return resp.json(false);
+          return resp.json({ success: false });
         }
         bcrypt.encrypt(req.body.pass, salt, function(err, hash) {
           if (err) {
             logger.error("error generating password hash with bcrypt: " + err);
-            return resp.json(false);
+            return resp.json({ success: false });
           }
 
           db.gotVerificationSecret(req.body.token, hash, function(err, email) {
             if (err) {
               logger.error("error completing the verification: " + err);
-              resp.json(false);
+              resp.json({ success: false });
             } else {
               // FIXME: not sure if we want to do this (ba)
               // at this point the user has set a password associated with an email address
               // that they've verified.  We create an authenticated session.
               setAuthenticatedUser(req.session, email);
-              resp.json(true);
+              resp.json({ success: true });
             }
           });
         });
@@ -286,7 +286,7 @@ function setup(app) {
         // store the email being added in session data
         req.session.pendingAddition = secret;
 
-        resp.json(true);
+        resp.json({ success: true });
 
         // let's now kick out a verification email!
         email.sendAddAddressEmail(req.body.email, req.body.site, secret);
@@ -299,7 +299,7 @@ function setup(app) {
 
   app.get('/wsapi/email_for_token', checkParams(["token"]), function(req,resp) {
     db.emailForVerificationSecret(req.query.token, function(email) {
-      resp.json({email: email});
+      resp.json({ email: email });
     });
   });
 
@@ -327,16 +327,16 @@ function setup(app) {
       function(registered) {
         if (registered) {
           delete req.session.pendingAddition;
-          resp.json('complete');
+          resp.json({ status: 'complete' });
         } else if (!req.session.pendingAddition) {
           resp.json('failed');
         } else {
           db.emailForVerificationSecret(req.session.pendingAddition, function (email) {
             if (email) {
-              return resp.json('pending');
+              return resp.json({ status: 'pending' });
             } else {
               delete req.session.pendingAddition;
-              resp.json('failed');
+              resp.json({ status: 'failed' });
             }
           });
         }
@@ -347,9 +347,9 @@ function setup(app) {
     db.gotVerificationSecret(req.body.token, undefined, function(e) {
       if (e) {
         logger.error("error completing the verification: " + e);
-        resp.json(false);
+        resp.json({ success: false });
       } else {
-        resp.json(true);
+        resp.json({ success: true });
       }
     });
   });
@@ -359,7 +359,7 @@ function setup(app) {
       if (typeof hash !== 'string' ||
           typeof req.body.pass !== 'string')
       {
-        return resp.json(false);
+        return resp.json({ success: false });
       }
 
       bcrypt.compare(req.body.pass, hash, function (err, success) {
@@ -373,7 +373,7 @@ function setup(app) {
 
           // if the work factor has changed, update the hash here
         }
-        resp.json(success);
+        resp.json({ success: success });
       });
     });
   });
@@ -386,7 +386,7 @@ function setup(app) {
         logger.error("error removing email " + email);
         httputils.badRequest(resp, error.toString());
       } else {
-        resp.json(true);
+        resp.json({ success: true });
       }});
   });
 
@@ -396,7 +396,7 @@ function setup(app) {
         logger.error("error cancelling account : " + error.toString());
         httputils.badRequest(resp, error.toString());
       } else {
-        resp.json(true);
+        resp.json({ success: true });
       }});
   });
 
@@ -422,7 +422,7 @@ function setup(app) {
 
   app.post('/wsapi/logout', function(req, resp) {
     clearAuthenticatedUser(req.session);
-    resp.json('ok');
+    resp.json({ success: true });
   });
 
   // in the cert world, syncing is not necessary,
diff --git a/browserid/static/dialog/resources/network.js b/browserid/static/dialog/resources/network.js
index 0cc838236..18d041256 100644
--- a/browserid/static/dialog/resources/network.js
+++ b/browserid/static/dialog/resources/network.js
@@ -125,7 +125,7 @@ BrowserID.Network = (function() {
         success: function(status, textStatus, jqXHR) {
           if (onSuccess) {
             try {
-              var authenticated = JSON.parse(status);
+              var authenticated = status.success;
 
               if (typeof authenticated !== 'boolean') throw status;
 
@@ -197,7 +197,7 @@ BrowserID.Network = (function() {
           site : origin
         },
         success: function(status) {
-          var staged = JSON.parse(status);
+          var staged = status.success;
           // why a delay here? Because of the test harness?
           // shouldn't the delay be in the test harness?
           _.delay(onSuccess, 0, staged);
@@ -233,7 +233,11 @@ BrowserID.Network = (function() {
     checkUserRegistration: function(email, onSuccess, onFailure) {
       xhr.ajax({
         url: "/wsapi/user_creation_status?email=" + encodeURIComponent(email),
-        success: createDeferred(onSuccess),
+        success: function(status, textStatus, jqXHR) {
+          if (onSuccess) {
+            _.delay(onSuccess, 0, status.status);
+          }
+        },
         error: onFailure
       });
     },
@@ -255,8 +259,7 @@ BrowserID.Network = (function() {
         },
         success: function(status, textStatus, jqXHR) {
           if (onSuccess) {
-            var valid = JSON.parse(status);
-            _.delay(onSuccess, 0, valid);
+            _.delay(onSuccess, 0, status.success);
           }
         },
         error: onFailure
@@ -325,8 +328,7 @@ BrowserID.Network = (function() {
         },
         success: function(status, textStatus, jqXHR) {
           if (onSuccess) {
-            var valid = JSON.parse(status);
-            _.delay(onSuccess, 0, valid);
+            _.delay(onSuccess, 0, status.success);
           }
         },
         error: onFailure
@@ -363,7 +365,7 @@ BrowserID.Network = (function() {
           site: origin
         },
         success: function(status) {
-          var staged = JSON.parse(status);
+          var staged = status.success;
           _.delay(onSuccess, 0, staged);
         },
         error: onFailure
@@ -380,7 +382,11 @@ BrowserID.Network = (function() {
     checkEmailRegistration: function(email, onSuccess, onFailure) {
       xhr.ajax({
         url: "/wsapi/email_addition_status?email=" + encodeURIComponent(email),
-        success: createDeferred(onSuccess),
+        success: function(status, textStatus, jqXHR) {
+          if (onSuccess) {
+            _.delay(onSuccess, 0, status.status);
+          }
+        },
         error: onFailure
       });
     },
@@ -399,8 +405,7 @@ BrowserID.Network = (function() {
         url: "/wsapi/have_email?email=" + encodeURIComponent(email),
         success: function(data, textStatus, xhr) {
           if(onSuccess) {
-            var success = typeof data === "string" ? !JSON.parse(data) : data;
-            _.delay(onSuccess, 0, success);
+            _.delay(onSuccess, 0, data.email_known);
           }
         },
         error: onFailure
@@ -420,7 +425,11 @@ BrowserID.Network = (function() {
         data: {
           email: email
         },
-        success: createDeferred(onSuccess),
+        success: function(status, textStatus, jqXHR) {
+          if (onSuccess) {
+            _.delay(onSuccess, 0, status.success);
+          }
+        },
         failure: onFailure
       });
     },
diff --git a/browserid/static/dialog/test/qunit/network_unit_test.js b/browserid/static/dialog/test/qunit/network_unit_test.js
index 4da6a9f18..1f532b24b 100644
--- a/browserid/static/dialog/test/qunit/network_unit_test.js
+++ b/browserid/static/dialog/test/qunit/network_unit_test.js
@@ -42,7 +42,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   function wrappedAsyncTest(name, test) {
     asyncTest(name, function() {
       testName = name;
-      test(); 
+      test();
     });
   }
 
@@ -56,38 +56,36 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
         server_time: new Date().getTime(),
         csrf_token: "csrf",
         authenticated: false
-      }; 
+      };
 
 
   var xhr = {
     results: {
       "get /wsapi/session_context valid": contextInfo,
       "get /wsapi/session_context invalid": contextInfo,
-      "post /wsapi/authenticate_user valid": "true",  
-      "post /wsapi/authenticate_user invalid": "false",
-      "get /wsapi/am_authed valid": "true",
-      "get /wsapi/am_authed invalid": "false",
-      "post /wsapi/complete_email_addition valid": "true",
-      "post /wsapi/complete_email_addition invalid": "false",
-      "post /wsapi/stage_user valid": "true",
-      "post /wsapi/stage_user invalid": "false",
+      "post /wsapi/authenticate_user valid": { success: true },
+      "post /wsapi/authenticate_user invalid": { success: false },
+      "post /wsapi/complete_email_addition valid": { success: true },
+      "post /wsapi/complete_email_addition invalid": { success: false },
+      "post /wsapi/stage_user valid": { success: true },
+      "post /wsapi/stage_user invalid": { success: false },
       "get /wsapi/user_creation_status?email=address notcreated": undefined, // undefined because server returns 400 error
-      "get /wsapi/user_creation_status?email=address pending": "pending",
-      "get /wsapi/user_creation_status?email=address complete": "complete",
-      "post /wsapi/complete_user_creation valid": "true",
-      "post /wsapi/complete_user_creation invalid": "false",
-      "post /wsapi/logout valid": "true",
-      "get /wsapi/have_email?email=address taken": "false",
-      "get /wsapi/have_email?email=address nottaken" : "true",
-      "post /wsapi/remove_email valid": "true",
-      "post /wsapi/remove_email invalid": "false",
-      "post /wsapi/account_cancel valid": "true",
-      "post /wsapi/account_cancel invalid": "false",
-      "post /wsapi/stage_email valid": "true",
-      "post /wsapi/stage_email invalid": "false",
+      "get /wsapi/user_creation_status?email=address pending": { status: "pending" },
+      "get /wsapi/user_creation_status?email=address complete": { status: "complete" },
+      "post /wsapi/complete_user_creation valid": { success: true },
+      "post /wsapi/complete_user_creation invalid": { success: false },
+      "post /wsapi/logout valid": { success: true },
+      "get /wsapi/have_email?email=address taken": { email_known: true },
+      "get /wsapi/have_email?email=address nottaken" : { email_known: false },
+      "post /wsapi/remove_email valid": { success: true },
+      "post /wsapi/remove_email invalid": { success: false },
+      "post /wsapi/account_cancel valid": { success: true },
+      "post /wsapi/account_cancel invalid": { success: false },
+      "post /wsapi/stage_email valid": { success: true },
+      "post /wsapi/stage_email invalid": { success: false },
       "get /wsapi/email_addition_status?email=address notcreated": undefined, // undefined because server returns 400 error
-      "get /wsapi/email_addition_status?email=address pending": "pending",
-      "get /wsapi/email_addition_status?email=address complete": "complete",
+      "get /wsapi/email_addition_status?email=address pending": { status: "pending" },
+      "get /wsapi/email_addition_status?email=address complete": { status: "complete" },
     },
 
     useResult: function(result) {
diff --git a/browserid/tests/cert-emails-test.js b/browserid/tests/cert-emails-test.js
index afdf9a5d0..2b3ef1caf 100755
--- a/browserid/tests/cert-emails-test.js
+++ b/browserid/tests/cert-emails-test.js
@@ -84,7 +84,7 @@ suite.addBatch({
     },
     "works": function(r, err) {
       assert.equal(r.code, 200);
-      assert.strictEqual(true, JSON.parse(r.body));
+      assert.strictEqual(true, JSON.parse(r.body).success);
     }
   }
 });
diff --git a/browserid/tests/forgotten-email-test.js b/browserid/tests/forgotten-email-test.js
index 13da60e37..8fa595b46 100755
--- a/browserid/tests/forgotten-email-test.js
+++ b/browserid/tests/forgotten-email-test.js
@@ -75,7 +75,7 @@ suite.addBatch({
     },
     "account created": function(r, err) {
       assert.equal(r.code, 200);
-      assert.strictEqual(true, JSON.parse(r.body));
+      assert.strictEqual(true, JSON.parse(r.body).success);
     }
   }
 });
@@ -85,7 +85,7 @@ suite.addBatch({
     topic: wsapi.get('/wsapi/user_creation_status', { email: 'first@fakeemail.com' } ),
     "should exist": function(r, err) {
       assert.strictEqual(r.code, 200);
-      assert.strictEqual(JSON.parse(r.body), "complete");
+      assert.strictEqual(JSON.parse(r.body).status, "complete");
     }
   }
 });
@@ -111,7 +111,7 @@ suite.addBatch({
     },
     "account created": function(r, err) {
       assert.equal(r.code, 200);
-      assert.strictEqual(true, JSON.parse(r.body));
+      assert.strictEqual(JSON.parse(r.body).success, true);
     }
   }
 });
@@ -121,19 +121,19 @@ suite.addBatch({
   "first email exists": {
     topic: wsapi.get('/wsapi/have_email', { email: 'first@fakeemail.com' }),
     "should exist": function(r, err) {
-      assert.strictEqual(true, JSON.parse(r.body));
+      assert.strictEqual(JSON.parse(r.body).email_known, true);
     }
   },
   "second email exists": {
     topic: wsapi.get('/wsapi/have_email', { email: 'second@fakeemail.com' }),
     "should exist": function(r, err) {
-      assert.strictEqual(JSON.parse(r.body), true);
+      assert.strictEqual(JSON.parse(r.body).email_known, true);
     }
   },
   "a random email doesn't exist": {
     topic: wsapi.get('/wsapi/have_email', { email: 'third@fakeemail.com' }),
     "shouldn't exist": function(r, err) {
-      assert.strictEqual(JSON.parse(r.body), false);
+      assert.strictEqual(JSON.parse(r.body).email_known, false);
     }
   }
 });
@@ -158,13 +158,13 @@ suite.addBatch({
   "first email works": {
     topic: wsapi.post('/wsapi/authenticate_user', { email: 'first@fakeemail.com', pass: 'firstfakepass' }),
     "should work": function(r, err) {
-      assert.strictEqual(true, JSON.parse(r.body));
+      assert.strictEqual(JSON.parse(r.body).success, true);
     }
   },
   "second email works": {
     topic: wsapi.post('/wsapi/authenticate_user', { email: 'second@fakeemail.com', pass: 'firstfakepass' }),
     "should work": function(r, err) {
-      assert.strictEqual(true, JSON.parse(r.body));
+      assert.strictEqual(JSON.parse(r.body).success, true);
     }
   }
 });
@@ -177,7 +177,7 @@ suite.addBatch({
     },
     "account created": function(r, err) {
       assert.equal(r.code, 200);
-      assert.strictEqual(true, JSON.parse(r.body));
+      assert.strictEqual(JSON.parse(r.body).success, true);
     }
   }
 });
@@ -188,31 +188,31 @@ suite.addBatch({
   "first email, first pass bad": {
     topic: wsapi.post('/wsapi/authenticate_user', { email: 'first@fakeemail.com', pass: 'firstfakepass' }),
     "shouldn't work": function(r, err) {
-      assert.strictEqual(JSON.parse(r.body), false);
+      assert.strictEqual(JSON.parse(r.body).success, false);
     }
   },
   "first email, second pass good": {
     topic: wsapi.post('/wsapi/authenticate_user', { email: 'first@fakeemail.com', pass: 'secondfakepass' }),
     "should work": function(r, err) {
-      assert.strictEqual(JSON.parse(r.body), true);
+      assert.strictEqual(JSON.parse(r.body).success, true);
     }
   },
   "logout": {
     topic: wsapi.post('/wsapi/logout', {}),
-      "should work": function(r, err) {
-      assert.strictEqual(JSON.parse(r.body), "ok");
+    "should work": function(r, err) {
+      assert.strictEqual(JSON.parse(r.body).success, true);
     }
   },
   "second email, first pass good": {
     topic: wsapi.post('/wsapi/authenticate_user', { email: 'second@fakeemail.com', pass: 'firstfakepass' }),
     "should work": function(r, err) {
-      assert.strictEqual(JSON.parse(r.body), true);
+      assert.strictEqual(JSON.parse(r.body).success, true);
     }
   },
   "second email, second pass bad": {
     topic: wsapi.post('/wsapi/authenticate_user', { email: 'second@fakeemail.com', pass: 'secondfakepass' }),
     "shouldn' work": function(r, err) {
-      assert.strictEqual(JSON.parse(r.body), false);
+      assert.strictEqual(JSON.parse(r.body).success, false);
     }
   },
 });
diff --git a/browserid/tests/list-emails-wsapi-test.js b/browserid/tests/list-emails-wsapi-test.js
index a99532318..e31480e25 100755
--- a/browserid/tests/list-emails-wsapi-test.js
+++ b/browserid/tests/list-emails-wsapi-test.js
@@ -75,7 +75,7 @@ suite.addBatch({
     },
     "works": function(r, err) {
       assert.equal(r.code, 200);
-      assert.strictEqual(JSON.parse(r.body), true);
+      assert.strictEqual(JSON.parse(r.body).success, true);
     }
   }
 });
@@ -87,7 +87,7 @@ suite.addBatch({
       assert.strictEqual(r.code, 200);
     },
     "returns a json encoded string - `complete`": function (r, err) {
-      assert.strictEqual(JSON.parse(r.body), "complete");
+      assert.strictEqual(JSON.parse(r.body).status, "complete");
     }
   }
 });
diff --git a/browserid/tests/registration-status-wsapi-test.js b/browserid/tests/registration-status-wsapi-test.js
index 152d1f40c..b4bc4be33 100755
--- a/browserid/tests/registration-status-wsapi-test.js
+++ b/browserid/tests/registration-status-wsapi-test.js
@@ -70,7 +70,7 @@ suite.addBatch({
   "authentication as an unknown user": {
     topic: wsapi.post('/wsapi/authenticate_user', { email: 'first@fakeemail.com', pass: 'secondfakepass' }),
     "fails": function (r, err) {
-      assert.isFalse(JSON.parse(r.body));
+      assert.isFalse(JSON.parse(r.body).success);
     }
   }
 });
@@ -117,7 +117,7 @@ suite.addBatch({
       assert.strictEqual(r.code, 200);
     },
     "returns a json encoded string - `pending`": function (r, err) {
-      assert.strictEqual(JSON.parse(r.body), "pending");
+      assert.strictEqual(JSON.parse(r.body).status, "pending");
     }
   }
 });
@@ -140,7 +140,7 @@ suite.addBatch({
       assert.strictEqual(r.code, 200);
     },
     "returns a json encoded string - `complete`": function (r, err) {
-      assert.strictEqual(JSON.parse(r.body), "complete");
+      assert.strictEqual(JSON.parse(r.body).status, "complete");
     }
   }
 });
@@ -152,7 +152,7 @@ suite.addBatch({
       assert.strictEqual(r.code, 200);
     },
     "and still returns a json encoded string - `complete`": function (r, err) {
-      assert.strictEqual(JSON.parse(r.body), "complete");
+      assert.strictEqual(JSON.parse(r.body).status, "complete");
     }
   }
 });
@@ -199,7 +199,7 @@ suite.addBatch({
       assert.strictEqual(r.code, 200);
     },
     "returns a json encoded string - `pending`": function (r, err) {
-      assert.strictEqual(JSON.parse(r.body), "pending");
+      assert.strictEqual(JSON.parse(r.body).status, "pending");
     }
   }
 });
@@ -222,7 +222,7 @@ suite.addBatch({
       assert.strictEqual(r.code, 200);
     },
     "returns a json encoded string - `complete`": function (r, err) {
-      assert.strictEqual(JSON.parse(r.body), "complete");
+      assert.strictEqual(JSON.parse(r.body).status, "complete");
     }
   }
 });
@@ -240,7 +240,7 @@ suite.addBatch({
   "after re-registration, authenticating with new credetials": {
     topic: wsapi.post('/wsapi/authenticate_user', { email: 'first@fakeemail.com', pass: 'secondfakepass' }),
     "works as you might expect": function (r, err) {
-      assert.strictEqual(true, JSON.parse(r.body));
+      assert.strictEqual(JSON.parse(r.body).success, true);
     }
   }
 });
-- 
GitLab