Bug 1238128 - Ensure that the details passed to WebChannelMessageToChrome is a string, with a whitelist for messages from existing users r?markh draft
authorThom Chiovoloni <tchiovoloni@mozilla.com>
Tue, 12 Jul 2016 19:34:41 -0400
changeset 387177 195d2748f55de15241d5052a4178127f2de2debc
parent 387176 3cf91a31e92c7b749999d74b1470b35ac1e97823
child 525296 a2960ccd6c2ea16f652aae1561495d4c74b2a8fb
push id22903
push userbmo:tchiovoloni@mozilla.com
push dateWed, 13 Jul 2016 14:16:11 +0000
reviewersmarkh
bugs1238128
milestone50.0a1
Bug 1238128 - Ensure that the details passed to WebChannelMessageToChrome is a string, with a whitelist for messages from existing users r?markh MozReview-Commit-ID: DpdJ5bUcBdQ
browser/app/profile/firefox.js
browser/base/content/test/general/browser_fxa_oauth.html
browser/base/content/test/general/browser_fxa_oauth.js
browser/base/content/test/general/browser_fxa_oauth_with_keys.html
browser/base/content/test/general/browser_fxa_web_channel.html
browser/base/content/test/general/browser_remoteTroubleshoot.js
browser/base/content/test/general/browser_web_channel.html
browser/base/content/test/general/browser_web_channel.js
browser/base/content/test/general/browser_web_channel_iframe.html
browser/base/content/test/general/test_remoteTroubleshoot.html
browser/components/newtab/tests/browser/browser_newtabwebchannel.js
browser/components/newtab/tests/browser/newtabmessages_places.html
browser/components/newtab/tests/browser/newtabmessages_prefs.html
browser/components/newtab/tests/browser/newtabmessages_preview.html
browser/components/newtab/tests/browser/newtabwebchannel_basic.html
browser/extensions/loop/chrome/content/shared/js/activeRoomStore.js
browser/extensions/loop/chrome/content/shared/test/activeRoomStore_test.js
browser/extensions/loop/chrome/test/mochitest/browser_LoopRooms_channel.js
browser/extensions/loop/chrome/test/mochitest/test_loopLinkClicker_channel.html
mobile/android/tests/browser/chrome/web_channel.html
toolkit/content/browser-content.js
toolkit/modules/WebChannel.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1449,8 +1449,11 @@ pref("dom.mozBrowserFramesEnabled", true
 
 pref("extensions.pocket.enabled", true);
 
 pref("signon.schemeUpgrades", true);
 
 // Enable the "Simplify Page" feature in Print Preview
 pref("print.use_simplify_page", true);
 
+// Space separated list of URLS that are allowed to send objects (instead of
+// only strings) through webchannels.
+pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://hello.firefox.com https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");
--- a/browser/base/content/test/general/browser_fxa_oauth.html
+++ b/browser/base/content/test/general/browser_fxa_oauth.html
@@ -3,16 +3,18 @@
 <head>
   <meta charset="utf-8">
   <title>fxa_oauth_test</title>
 </head>
 <body>
 <script>
   window.onload = function(){
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
+      // Note: This intentionally sends an object instead of a string, to ensure both work
+      // (see browser_fxa_oauth_with_keys.html for the other test)
       detail: {
         id: "oauth_client_id",
         message: {
           command: "oauth_complete",
           data: {
             state: "state",
             code: "code1",
             closeWindow: "signin",
--- a/browser/base/content/test/general/browser_fxa_oauth.js
+++ b/browser/base/content/test/general/browser_fxa_oauth.js
@@ -304,17 +304,25 @@ function waitForTab(aCallback) {
     }, true);
   }, false);
 }
 
 function test() {
   waitForExplicitFinish();
 
   Task.spawn(function () {
-    for (let test of gTests) {
-      info("Running: " + test.desc);
-      yield test.run();
+    const webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
+    let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
+    let newWhitelist = origWhitelist + " http://example.com";
+    Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
+    try {
+      for (let test of gTests) {
+        info("Running: " + test.desc);
+        yield test.run();
+      }
+    } finally {
+      Services.prefs.clearUserPref(webchannelWhitelistPref);
     }
   }).then(finish, ex => {
     Assert.ok(false, "Unexpected Exception: " + ex);
     finish();
   });
 }
--- a/browser/base/content/test/general/browser_fxa_oauth_with_keys.html
+++ b/browser/base/content/test/general/browser_fxa_oauth_with_keys.html
@@ -3,29 +3,31 @@
 <head>
   <meta charset="utf-8">
   <title>fxa_oauth_test</title>
 </head>
 <body>
 <script>
   window.onload = function(){
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      // Note: This intentionally sends a string instead of an object, to ensure both work
+      // (see browser_fxa_oauth.html for the other test)
+      detail: JSON.stringify({
         id: "oauth_client_id",
         message: {
           command: "oauth_complete",
           data: {
             state: "state",
             code: "code1",
             closeWindow: "signin",
             // Keys normally contain more information, but this is enough
             // to keep Loop's tests happy.
             keys: { kAr: { k: 'kAr' }, kBr: { k: 'kBr' }},
           },
         },
-      },
+      }),
     });
 
     window.dispatchEvent(event);
   };
 </script>
 </body>
 </html>
--- a/browser/base/content/test/general/browser_fxa_web_channel.html
+++ b/browser/base/content/test/general/browser_fxa_web_channel.html
@@ -27,112 +27,112 @@
       case "delete":
         test_delete();
         break;
     }
   };
 
   function test_profile_change() {
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: webChannelId,
         message: {
           command: "profile:change",
           data: {
             uid: "abc123",
           },
         },
-      },
+      }),
     });
 
     window.dispatchEvent(event);
   }
 
   function test_login() {
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: webChannelId,
         message: {
           command: "fxaccounts:login",
           data: {
             authAt: Date.now(),
             email: "testuser@testuser.com",
             keyFetchToken: 'key_fetch_token',
             sessionToken: 'session_token',
             uid: 'uid',
             unwrapBKey: 'unwrap_b_key',
             verified: true,
           },
           messageId: 1,
         },
-      },
+      }),
     });
 
     window.dispatchEvent(event);
   }
 
   function test_can_link_account() {
     window.addEventListener("WebChannelMessageToContent", function (e) {
       // echo any responses from the browser back to the tests on the
       // fxaccounts_webchannel_response_echo WebChannel. The tests are
       // listening for events and do the appropriate checks.
       var event = new window.CustomEvent("WebChannelMessageToChrome", {
-        detail: {
+        detail: JSON.stringify({
           id: 'fxaccounts_webchannel_response_echo',
           message: e.detail.message,
-        }
+        })
       });
 
       window.dispatchEvent(event);
     }, true);
 
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: webChannelId,
         message: {
           command: "fxaccounts:can_link_account",
           data: {
             email: "testuser@testuser.com",
           },
           messageId: 2,
         },
-      },
+      }),
     });
 
     window.dispatchEvent(event);
   }
 
   function test_logout() {
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: webChannelId,
         message: {
           command: "fxaccounts:logout",
           data: {
             uid: 'uid'
           },
           messageId: 3,
         },
-      },
+      }),
     });
 
     window.dispatchEvent(event);
   }
 
   function test_delete() {
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: webChannelId,
         message: {
           command: "fxaccounts:delete",
           data: {
             uid: 'uid'
           },
           messageId: 4,
         },
-      },
+      }),
     });
 
     window.dispatchEvent(event);
   }
 </script>
 </body>
 </html>
--- a/browser/base/content/test/general/browser_remoteTroubleshoot.js
+++ b/browser/base/content/test/general/browser_remoteTroubleshoot.js
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var {WebChannel} = Cu.import("resource://gre/modules/WebChannel.jsm", {});
 
 const TEST_URL_TAIL = "example.com/browser/browser/base/content/test/general/test_remoteTroubleshoot.html"
 const TEST_URI_GOOD = Services.io.newURI("https://" + TEST_URL_TAIL, null, null);
 const TEST_URI_BAD = Services.io.newURI("http://" + TEST_URL_TAIL, null, null);
+const TEST_URI_GOOD_OBJECT = Services.io.newURI("https://" + TEST_URL_TAIL + "?object", null, null);
 
 // Creates a one-shot web-channel for the test data to be sent back from the test page.
 function promiseChannelResponse(channelID, originOrPermission) {
   return new Promise((resolve, reject) => {
     let channel = new WebChannel(channelID, originOrPermission);
     channel.listen((id, data, target) => {
       channel.stopListening();
       resolve(data);
@@ -73,9 +74,20 @@ add_task(function*() {
 
   // And check some keys we know we decline to return.
   Assert.ok(!got.message.modifiedPreferences, "should not have a modifiedPreferences key");
   Assert.ok(!got.message.crashes, "should not have crash info");
 
   // Now a http:// URI - should get nothing even with the permission setup.
   got = yield promiseNewChannelResponse(TEST_URI_BAD);
   Assert.ok(got.message === undefined, "should have failed to get any data");
+
+  // Check that the page can send an object as well if it's in the whitelist
+  let webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
+  let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
+  let newWhitelist = origWhitelist + " https://example.com";
+  Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref(webchannelWhitelistPref);
+  });
+  got = yield promiseNewChannelResponse(TEST_URI_GOOD_OBJECT);
+  Assert.ok(got.message, "should have gotten some data back");
 });
--- a/browser/base/content/test/general/browser_web_channel.html
+++ b/browser/base/content/test/general/browser_web_channel.html
@@ -28,79 +28,82 @@
         test_iframe_pre_redirect();
         break;
       case "unsolicited":
         test_unsolicited();
         break;
       case "bubbles":
         test_bubbles();
         break;
+      case "object":
+        test_object();
+        break;
       default:
         throw new Error(`INVALID TEST NAME ${testName}`);
         break;
     }
   };
 
   function test_generic() {
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "generic",
         message: {
           something: {
             nested: "hello",
           },
         }
-      }
+      })
     });
 
     window.dispatchEvent(event);
   }
 
   function test_twoWay() {
     var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "twoway",
         message: {
           command: "one",
         },
-      }
+      })
     });
 
     window.addEventListener("WebChannelMessageToContent", function(e) {
       var secondMessage = new window.CustomEvent("WebChannelMessageToChrome", {
-        detail: {
+        detail: JSON.stringify({
           id: "twoway",
           message: {
             command: "two",
             detail: e.detail.message,
           },
-        },
+        }),
       });
 
       if (!e.detail.message.error) {
         window.dispatchEvent(secondMessage);
       }
     }, true);
 
     window.dispatchEvent(firstMessage);
   }
 
   function test_multichannel() {
     var event1 = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "wrongchannel",
         message: {},
-      }
+      })
     });
 
     var event2 = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "multichannel",
         message: {},
-      }
+      })
     });
 
     window.dispatchEvent(event1);
     window.dispatchEvent(event2);
   }
 
   function test_iframe() {
     // Note that this message is the response to the message sent
@@ -127,40 +130,60 @@
     // echo any unsolicted events back to chrome.
     window.addEventListener("WebChannelMessageToContent", function(e) {
       echoEventToChannel(e, "echo");
     }, true);
   }
 
   function test_bubbles() {
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "not_a_window",
         message: {
           command: "start"
         }
-      }
+      })
     });
 
     var nonWindowTarget = document.getElementById("not_a_window");
 
     nonWindowTarget.addEventListener("WebChannelMessageToContent", function(e) {
       echoEventToChannel(e, "not_a_window");
     }, true);
 
 
     nonWindowTarget.dispatchEvent(event);
   }
 
+  function test_object() {
+    let objectMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+      detail: {
+        id: "objects",
+        message: { type: "object" }
+      }
+    });
+
+    let stringMessage = new window.CustomEvent("WebChannelMessageToChrome", {
+      detail: JSON.stringify({
+        id: "objects",
+        message: { type: "string" }
+      })
+    });
+    // Test fails if objectMessage is received, we send stringMessage to know
+    // when we should stop listening for objectMessage
+    window.dispatchEvent(objectMessage);
+    window.dispatchEvent(stringMessage);
+  }
+
   function echoEventToChannel(e, channelId) {
     var echoedEvent = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: channelId,
         message: e.detail.message,
-      }
+      })
     });
 
     e.target.dispatchEvent(echoedEvent);
   }
 </script>
 
 <div id="not_a_window"></div>
 </body>
--- a/browser/base/content/test/general/browser_web_channel.js
+++ b/browser/base/content/test/general/browser_web_channel.js
@@ -39,18 +39,18 @@ var gTests = [
   {
     desc: "WebChannel two way communication",
     run: function* () {
       return new Promise(function(resolve, reject) {
         let tab;
         let channel = new WebChannel("twoway", Services.io.newURI(HTTP_PATH, null, null));
 
         channel.listen(function (id, message, sender) {
-          is(id, "twoway");
-          ok(message.command);
+          is(id, "twoway", "bad id");
+          ok(message.command, "command not ok");
 
           if (message.command === "one") {
             channel.send({ data: { nested: true } }, sender);
           }
 
           if (message.command === "two") {
             is(message.detail.data.nested, true);
             channel.stopListening();
@@ -69,18 +69,18 @@ var gTests = [
       let parentChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH, null, null));
       let iframeChannel = new WebChannel("twoway", Services.io.newURI(HTTP_IFRAME_PATH, null, null));
       let promiseTestDone = new Promise(function (resolve, reject) {
         parentChannel.listen(function (id, message, sender) {
           reject(new Error("WebChannel message incorrectly sent to parent"));
         });
 
         iframeChannel.listen(function (id, message, sender) {
-          is(id, "twoway");
-          ok(message.command);
+          is(id, "twoway", "bad id (2)");
+          ok(message.command, "command not ok (2)");
 
           if (message.command === "one") {
             iframeChannel.send({ data: { nested: true } }, sender);
           }
 
           if (message.command === "two") {
             is(message.detail.data.nested, true);
             resolve();
@@ -323,16 +323,85 @@ var gTests = [
         gBrowser,
         url: HTTP_PATH + HTTP_ENDPOINT + "?bubbles"
       }, function* () {
         yield testDonePromise;
         channel.stopListening();
       });
     }
   },
+  {
+    desc: "WebChannel disallows non-string message from non-whitelisted origin",
+    run: function* () {
+      /**
+       * This test ensures that non-string messages can't be sent via WebChannels.
+       * We create a page (on a non-whitelisted origin) which should send us two
+       * messages immediately. The first message has an object for it's detail,
+       * and the second has a string. We check that we only get the second
+       * message.
+       */
+      let channel = new WebChannel("objects", Services.io.newURI(HTTP_PATH, null, null));
+      let testDonePromise = new Promise((resolve, reject) => {
+        channel.listen((id, message, sender) => {
+          is(id, "objects");
+          is(message.type, "string");
+          resolve();
+        });
+      });
+      yield BrowserTestUtils.withNewTab({
+        gBrowser,
+        url: HTTP_PATH + HTTP_ENDPOINT + "?object"
+      }, function* () {
+        yield testDonePromise;
+        channel.stopListening();
+      });
+    }
+  },
+  {
+    desc: "WebChannel allows both string and non-string message from whitelisted origin",
+    run: function* () {
+      /**
+       * Same process as above, but we whitelist the origin before loading the page,
+       * and expect to get *both* messages back (each exactly once).
+       */
+      let channel = new WebChannel("objects", Services.io.newURI(HTTP_PATH, null, null));
+
+      let testDonePromise = new Promise((resolve, reject) => {
+        let sawObject = false;
+        let sawString = false;
+        channel.listen((id, message, sender) => {
+          is(id, "objects");
+          if (message.type === "object") {
+            ok(!sawObject);
+            sawObject = true;
+          } else if (message.type === "string") {
+            ok(!sawString);
+            sawString = true;
+          } else {
+            reject(new Error(`Unknown message type: ${message.type}`))
+          }
+          if (sawObject && sawString) {
+            resolve();
+          }
+        });
+      });
+      const webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
+      let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
+      let newWhitelist = origWhitelist + " " + HTTP_PATH;
+      Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
+      yield BrowserTestUtils.withNewTab({
+        gBrowser,
+        url: HTTP_PATH + HTTP_ENDPOINT + "?object"
+      }, function* () {
+        yield testDonePromise;
+        Services.prefs.setCharPref(webchannelWhitelistPref, origWhitelist);
+        channel.stopListening();
+      });
+    }
+  }
 ]; // gTests
 
 function test() {
   waitForExplicitFinish();
 
   Task.spawn(function () {
     for (let test of gTests) {
       info("Running: " + test.desc);
--- a/browser/base/content/test/general/browser_web_channel_iframe.html
+++ b/browser/base/content/test/general/browser_web_channel_iframe.html
@@ -23,75 +23,75 @@
       default:
         throw new Error(`INVALID TEST NAME ${testName}`);
         break;
     }
   };
 
   function test_iframe() {
     var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "twoway",
         message: {
           command: "one",
         },
-      }
+      })
     });
 
     window.addEventListener("WebChannelMessageToContent", function(e) {
       var secondMessage = new window.CustomEvent("WebChannelMessageToChrome", {
-        detail: {
+        detail: JSON.stringify({
           id: "twoway",
           message: {
             command: "two",
             detail: e.detail.message,
           },
-        },
+        }),
       });
 
       if (!e.detail.message.error) {
         window.dispatchEvent(secondMessage);
       }
     }, true);
 
     window.dispatchEvent(firstMessage);
   }
 
 
   function test_iframe_pre_redirect() {
     var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "pre_redirect",
         message: {
           command: "redirecting",
         },
-      },
+      }),
     });
     window.dispatchEvent(firstMessage);
     document.location = REDIRECTED_IFRAME_SRC_ROOT + "?iframe_post_redirect";
   }
 
   function test_iframe_post_redirect() {
     window.addEventListener("WebChannelMessageToContent", function(e) {
       var echoMessage = new window.CustomEvent("WebChannelMessageToChrome", {
-        detail: {
+        detail: JSON.stringify({
           id: "post_redirect",
           message: e.detail.message,
-        },
+        }),
       });
 
       window.dispatchEvent(echoMessage);
     }, true);
 
     // Let the test parent know the page has loaded and is ready to echo events
     var loadedMessage = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "post_redirect",
         message: {
           command: "loaded",
         },
-      },
+      }),
     });
     window.dispatchEvent(loadedMessage);
   }
 </script>
 </body>
 </html>
--- a/browser/base/content/test/general/test_remoteTroubleshoot.html
+++ b/browser/base/content/test/general/test_remoteTroubleshoot.html
@@ -1,39 +1,48 @@
 <!DOCTYPE HTML>
 <html>
 <script>
+// This test is run multiple times, once with only strings allowed through the
+// WebChannel, and once with objects allowed. This function allows us to handle
+// both cases without too much pain.
+function makeDetails(object) {
+  if (window.location.search.indexOf("object") >= 0) {
+    return object;
+  }
+  return JSON.stringify(object)
+}
 // Add a listener for responses to our remote requests.
 window.addEventListener("WebChannelMessageToContent", function (event) {
   if (event.detail.id == "remote-troubleshooting") {
     // Send what we got back to the test.
     var backEvent = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: makeDetails({
         id: "test-remote-troubleshooting-backchannel",
         message: {
           message: event.detail.message,
         },
-      },
+      }),
     });
     window.dispatchEvent(backEvent);
     // and stick it in our DOM just for good measure/diagnostics.
     document.getElementById("troubleshooting").textContent =
       JSON.stringify(event.detail.message, null, 2);
   }
 });
 
 // Make a request for the troubleshooting data as we load.
 window.onload = function() {
   var event = new window.CustomEvent("WebChannelMessageToChrome", {
-    detail: {
+    detail: makeDetails({
       id: "remote-troubleshooting",
       message: {
         command: "request",
       },
-    },
+    }),
   });
   window.dispatchEvent(event);
 }
 </script>
 
 <body>
   <pre id="troubleshooting"/>
 </body>
--- a/browser/components/newtab/tests/browser/browser_newtabwebchannel.js
+++ b/browser/components/newtab/tests/browser/browser_newtabwebchannel.js
@@ -147,22 +147,24 @@ add_task(function* webchannel_switch() {
     return new Promise(resolve => {
       NewTabWebChannel.once("foo", function(name, msg) {
         resolve(msg.target);
       }.bind(this));
     });
   }
 
   let replyCount = 0;
-  let replyPromise = new Promise(resolve => {
-    NewTabWebChannel.on("reply", function() {
-      replyCount += 1;
-      resolve();
-    }.bind(this));
-  });
+  function newReplyPromise() {
+    return new Promise(resolve => {
+      NewTabWebChannel.on("reply", function() {
+        replyCount += 1;
+        resolve();
+      });
+    });
+  }
 
   let unloadPromise = new Promise(resolve => {
     NewTabWebChannel.once("targetUnload", function() {
       resolve();
     });
   });
 
   let unloadAllPromise = new Promise(resolve => {
@@ -183,19 +185,30 @@ add_task(function* webchannel_switch() {
   messagePromise = newMessagePromise();
   Preferences.set("browser.newtabpage.remote.mode", "test2");
   tabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL_2));
   yield unloadAllPromise;
   yield messagePromise;
   is(NewTabWebChannel.numBrowsers, 1, "Correct number of targets");
 
   NewTabWebChannel.broadcast("respond", null);
-  yield replyPromise;
+  yield newReplyPromise();
   is(replyCount, 1, "only current channel is listened to for replies");
 
+  const webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
+  let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
+  let newWhitelist = origWhitelist + " http://mochi.test:8888";
+  Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
+  try {
+    NewTabWebChannel.broadcast("respond_object", null);
+    yield newReplyPromise();
+  } finally {
+    Services.prefs.clearUserPref(webchannelWhitelistPref);
+  }
+
   for (let tab of tabs) {
     yield BrowserTestUtils.removeTab(tab);
   }
 
   Cu.forceGC();
   is(NewTabWebChannel.numBrowsers, 0, "Sanity check");
   yield unloadPromise;
   cleanup();
--- a/browser/components/newtab/tests/browser/newtabmessages_places.html
+++ b/browser/components/newtab/tests/browser/newtabmessages_places.html
@@ -6,44 +6,44 @@
   <body>
     <script>
       window.addEventListener("WebChannelMessageToContent", function(e) {
         if (e.detail.message) {
           let reply;
           switch (e.detail.message.type) {
             case "RECEIVE_FRECENT":
               reply = new window.CustomEvent("WebChannelMessageToChrome", {
-                detail: {
+                detail: JSON.stringify({
                   id: "newtab",
                   message: JSON.stringify({type: "numItemsAck", data: e.detail.message.data.length}),
-                }
+                })
               });
               window.dispatchEvent(reply);
               break;
             case "RECEIVE_PLACES_CHANGE":
               if (e.detail.message.data.type === "clearHistory") {
                 reply = new window.CustomEvent("WebChannelMessageToChrome", {
-                  detail: {
+                  detail: JSON.stringify({
                     id: "newtab",
                     message: JSON.stringify({type: "clearHistoryAck", data: e.detail.message.data.type}),
-                  }
+                  })
                 });
                 window.dispatchEvent(reply);
               }
               break;
           }
         }
       }, true);
 
       document.onreadystatechange = function () {
         if (document.readyState === "complete") {
           let msg = new window.CustomEvent("WebChannelMessageToChrome", {
-            detail: {
+            detail: JSON.stringify({
               id: "newtab",
               message: JSON.stringify({type: "REQUEST_FRECENT"}),
-            }
+            })
           });
           window.dispatchEvent(msg);
         }
       }
     </script>
   </body>
 </html>
--- a/browser/components/newtab/tests/browser/newtabmessages_prefs.html
+++ b/browser/components/newtab/tests/browser/newtabmessages_prefs.html
@@ -3,30 +3,30 @@
         <meta charset="utf8">
         <title>Newtab WebChannel test</title>
     </head>
     <body>
         <script>
             window.addEventListener("WebChannelMessageToContent", function(e) {
                 if (e.detail.message && e.detail.message.type === "RECEIVE_PREFS") {
                     let reply = new window.CustomEvent("WebChannelMessageToChrome", {
-                        detail: {
+                        detail: JSON.stringify({
                             id: "newtab",
                             message: JSON.stringify({type: "responseAck"}),
-                        }
+                        })
                     });
                     window.dispatchEvent(reply);
                 }
             }, true);
 
             document.onreadystatechange = function () {
                 let msg = new window.CustomEvent("WebChannelMessageToChrome", {
-                    detail: {
+                    detail: JSON.stringify({
                         id: "newtab",
                         message: JSON.stringify({type: "REQUEST_PREFS"}),
-                    }
+                    })
                 });
                 window.dispatchEvent(msg);
             };
 
         </script>
     </body>
 </html>
--- a/browser/components/newtab/tests/browser/newtabmessages_preview.html
+++ b/browser/components/newtab/tests/browser/newtabmessages_preview.html
@@ -6,32 +6,32 @@
   <body>
     <script>
       let thumbURL = "https://example.com/browser/browser/components/newtab/tests/browser/blue_page.html";
 
       window.addEventListener("WebChannelMessageToContent", function(e) {
         if (e.detail.message && e.detail.message.type === "RECEIVE_THUMB") {
           if (e.detail.message.data.imgData && e.detail.message.data.url === thumbURL) {
             let reply = new window.CustomEvent("WebChannelMessageToChrome", {
-              detail: {
+              detail: JSON.stringify({
                 id: "newtab",
                 message: JSON.stringify({type: "responseAck"}),
-              }
+              })
             });
             window.dispatchEvent(reply);
           }
         }
       }, true);
 
       document.onreadystatechange = function () {
         if (document.readyState === "complete") {
           let msg = new window.CustomEvent("WebChannelMessageToChrome", {
-            detail: {
+            detail: JSON.stringify({
               id: "newtab",
               message: JSON.stringify({type: "REQUEST_THUMB", data: thumbURL}),
-            }
+            })
           });
           window.dispatchEvent(msg);
         }
       };
     </script>
   </body>
 </html>
--- a/browser/components/newtab/tests/browser/newtabwebchannel_basic.html
+++ b/browser/components/newtab/tests/browser/newtabwebchannel_basic.html
@@ -2,31 +2,35 @@
     <head>
         <meta charset="utf8">
         <title>Newtab WebChannel test</title>
     </head>
     <body>
         <script>
             document.onreadystatechange = function () {
                 let msg = new window.CustomEvent("WebChannelMessageToChrome", {
-                    detail: {
+                    detail: JSON.stringify({
                         id: "newtab",
                         message: JSON.stringify({type: "foo", data: "bar"}),
-                    }
+                    })
                 });
                 window.dispatchEvent(msg);
             };
 
             window.addEventListener("WebChannelMessageToContent", function(e) {
-                if (e.detail.message && e.detail.message.type === "respond") {
+                if (e.detail.message && e.detail.message.type.startsWith("respond")) {
+                    var detail = {
+                        id: "newtab",
+                        message: JSON.stringify({type: "reply", data: "quuz"}),
+                    };
+                    if (e.detail.message.type !== "respond_object") {
+                        detail = JSON.stringify(detail);
+                    }
                     let reply = new window.CustomEvent("WebChannelMessageToChrome", {
-                        detail: {
-                            id: "newtab",
-                            message: JSON.stringify({type: "reply", data: "quuz"}),
-                        }
+                        detail: detail
                     });
                     window.dispatchEvent(reply);
                 }
             }, true);
             
         </script>
     </body>
 </html>
--- a/browser/extensions/loop/chrome/content/shared/js/activeRoomStore.js
+++ b/browser/extensions/loop/chrome/content/shared/js/activeRoomStore.js
@@ -490,21 +490,21 @@ loop.store.ActiveRoomStore = function (m
 
 
         webChannelListenerFunc = webChannelListener.bind(this);
 
         window.addEventListener("WebChannelMessageToContent", webChannelListenerFunc);
 
         // Now send a message to the chrome to see if it can handle this room.
         window.dispatchEvent(new window.CustomEvent("WebChannelMessageToChrome", { 
-          detail: { 
+          detail: JSON.stringify({ 
             id: "loop-link-clicker", 
             message: { 
               command: "checkWillOpenRoom", 
-              roomToken: this._storeState.roomToken } } }));}.
+              roomToken: this._storeState.roomToken } }) }));}.
 
 
 
       bind(this));}, 
 
 
     /**
      * Handles the updateRoomInfo action. Updates the room data.
@@ -616,21 +616,21 @@ loop.store.ActiveRoomStore = function (m
 
 
       channelListener = handleRoomJoinResponse.bind(this);
 
       window.addEventListener("WebChannelMessageToContent", channelListener);
 
       // Now we're set up, dispatch an event.
       window.dispatchEvent(new window.CustomEvent("WebChannelMessageToChrome", { 
-        detail: { 
+        detail: JSON.stringify({ 
           id: "loop-link-clicker", 
           message: { 
             command: "openRoom", 
-            roomToken: this._storeState.roomToken } } }));}, 
+            roomToken: this._storeState.roomToken } }) }));}, 
 
 
 
 
 
     /**
      * Handles the action to join to a room.
      */
--- a/browser/extensions/loop/chrome/content/shared/test/activeRoomStore_test.js
+++ b/browser/extensions/loop/chrome/content/shared/test/activeRoomStore_test.js
@@ -885,21 +885,21 @@ describe("loop.store.ActiveRoomStore", f
       it("should dispatch an event to Firefox", function () {
         sandbox.stub(window, "dispatchEvent");
 
         store.joinRoom();
 
         sinon.assert.calledOnce(window.dispatchEvent);
         sinon.assert.calledWithExactly(window.dispatchEvent, new window.CustomEvent(
         "WebChannelMessageToChrome", { 
-          detail: { 
+          detail: JSON.stringify({ 
             id: "loop-link-clicker", 
             message: { 
               command: "openRoom", 
-              roomToken: "fakeToken" } } }));});
+              roomToken: "fakeToken" } }) }));});
 
 
 
 
 
       it("should log an error if Firefox doesn't handle the room", function () {
         // Start the join.
         store.joinRoom();
--- a/browser/extensions/loop/chrome/test/mochitest/browser_LoopRooms_channel.js
+++ b/browser/extensions/loop/chrome/test/mochitest/browser_LoopRooms_channel.js
@@ -10,16 +10,17 @@
 
 var { WebChannel } = Cu.import("resource://gre/modules/WebChannel.jsm", {});
 var { Chat } = Cu.import("resource:///modules/Chat.jsm", {});
 
 const TEST_URI =
   "example.com/browser/browser/extensions/loop/chrome/test/mochitest/test_loopLinkClicker_channel.html";
 const TEST_URI_GOOD = Services.io.newURI("https://" + TEST_URI, null, null);
 const TEST_URI_BAD = Services.io.newURI("http://" + TEST_URI, null, null);
+const TEST_URI_GOOD_OBJECT = Services.io.newURI("https://" + TEST_URI + "?object", null, null);
 
 const ROOM_TOKEN = "fake1234";
 const LINKCLICKER_URL_PREFNAME = "loop.linkClicker.url";
 
 var openChatOrig = Chat.open;
 
 var fakeRoomList = new Map([[ROOM_TOKEN, { roomToken: ROOM_TOKEN }]]);
 
@@ -160,9 +161,23 @@ add_task(function* test_loopRooms_webcha
 
   // Simulate a window already being open.
   MozLoopServiceInternal.mocks.isChatWindowOpen = true;
 
   got = yield promiseNewChannelResponse(TEST_URI_GOOD, gGoodBackChannel, "openRoom");
 
   Assert.equal(got.message.response, true, "should have got a response of true");
   Assert.equal(got.message.alreadyOpen, true, "should indicate the room is already open");
+
+  // Ensure this still works properly when passing an object through the WebChannel
+  let webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist";
+  let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref);
+  let newWhitelist = origWhitelist + " https://example.com";
+  Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist);
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref(webchannelWhitelistPref);
+  });
+  got = yield promiseNewChannelResponse(TEST_URI_GOOD_OBJECT, gGoodBackChannel, "openRoom");
+
+  Assert.equal(got.message.response, true, "should have got a response of true with objects");
+  Assert.equal(got.message.alreadyOpen, true, "should indicate the room is already open with objects");
+
 });
--- a/browser/extensions/loop/chrome/test/mochitest/test_loopLinkClicker_channel.html
+++ b/browser/extensions/loop/chrome/test/mochitest/test_loopLinkClicker_channel.html
@@ -1,44 +1,53 @@
 <!DOCTYPE HTML>
 <html>
 <script>
 "use strict";
+// This test is run multiple times, once with only strings allowed through the
+// WebChannel, and once with objects allowed. This function allows us to handle
+// both cases without too much pain.
+function makeDetails(object) {
+  if (window.location.search.indexOf("object") >= 0) {
+    return object;
+  }
+  return JSON.stringify(object)
+}
 
 // Add a listener for responses to our remote requests.
 window.addEventListener("WebChannelMessageToContent", function (event) {
   if (event.detail.id == "loop-link-clicker") {
     // Send what we got back to the test.
     var backEvent = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: makeDetails({
         id: "test-loop-link-clicker-backchannel",
         message: {
           message: event.detail.message
         }
-      }
+      })
     });
     window.dispatchEvent(backEvent);
     // and stick it in our DOM just for good measure/diagnostics.
     document.getElementById("troubleshooting").textContent =
       JSON.stringify(event.detail.message, null, 2);
   }
 });
 
 // Send a message on load requesting that the room is opened.
 window.onload = function() {
   var hash = window.location.hash;
 
   var event = new window.CustomEvent("WebChannelMessageToChrome", {
-    detail: {
+    detail: makeDetails({
       id: "loop-link-clicker",
       message: {
         command: hash.substring(1, hash.length),
         roomToken: "fake1234"
       }
-    }
+    })
   });
   window.dispatchEvent(event);
 };
 </script>
 
 <body>
   <pre id="troubleshooting"/>
 </body>
--- a/mobile/android/tests/browser/chrome/web_channel.html
+++ b/mobile/android/tests/browser/chrome/web_channel.html
@@ -19,71 +19,71 @@
       case "multichannel":
         test_multichannel();
         break;
     }
   };
 
   function test_generic() {
     var event = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "generic",
         message: {
           something: {
             nested: "hello",
           },
         }
-      }
+      })
     });
 
     window.dispatchEvent(event);
   }
 
   function test_twoWay() {
     var firstMessage = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "twoway",
         message: {
           command: "one",
         },
-      }
+      })
     });
 
     window.addEventListener("WebChannelMessageToContent", function(e) {
       var secondMessage = new window.CustomEvent("WebChannelMessageToChrome", {
-        detail: {
+        detail: JSON.stringify({
           id: "twoway",
           message: {
             command: "two",
             detail: e.detail.message,
           },
-        },
+        }),
       });
 
       if (!e.detail.message.error) {
         window.dispatchEvent(secondMessage);
       }
     }, true);
 
     window.dispatchEvent(firstMessage);
   }
 
   function test_multichannel() {
     var event1 = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "wrongchannel",
         message: {},
-      }
+      })
     });
 
     var event2 = new window.CustomEvent("WebChannelMessageToChrome", {
-      detail: {
+      detail: JSON.stringify({
         id: "multichannel",
         message: {},
-      }
+      })
     });
 
     window.dispatchEvent(event1);
     window.dispatchEvent(event2);
   }
 </script>
 </body>
 </html>
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -809,27 +809,66 @@ var FindBar = {
 
   _onMouseup(event) {
     if (this._findMode != this.FIND_NORMAL)
       sendAsyncMessage("Findbar:Mouseup");
   },
 };
 FindBar.init();
 
-// An event listener for custom "WebChannelMessageToChrome" events on pages.
-addEventListener("WebChannelMessageToChrome", function (e) {
-  // If target is window then we want the document principal, otherwise fallback to target itself.
-  let principal = e.target.nodePrincipal ? e.target.nodePrincipal : e.target.document.nodePrincipal;
+let WebChannelMessageToChromeListener = {
+  // Preference containing the list (space separated) of origins that are
+  // allowed to send non-string values through a WebChannel, mainly for
+  // backwards compatability. See bug 1238128 for more information.
+  URL_WHITELIST_PREF: "webchannel.allowObject.urlWhitelist",
+
+  // Cached list of whitelisted principals, we avoid constructing this if the
+  // value in `_lastWhitelistValue` hasn't changed since we constructed it last.
+  _cachedWhitelist: [],
+  _lastWhitelistValue: "",
+
+  init() {
+    addEventListener("WebChannelMessageToChrome", e => {
+      this._onMessageToChrome(e);
+    }, true, true);
+  },
 
-  if (e.detail) {
-    sendAsyncMessage("WebChannelMessageToChrome", e.detail, { eventTarget: e.target }, principal);
-  } else  {
-    Cu.reportError("WebChannel message failed. No message detail.");
+  _getWhitelistedPrincipals() {
+    let whitelist = Services.prefs.getCharPref(this.URL_WHITELIST_PREF);
+    if (whitelist != this._lastWhitelistValue) {
+      let urls = whitelist.split(/\s+/);
+      this._cachedWhitelist = urls.map(origin =>
+        Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin));
+    }
+    return this._cachedWhitelist;
+  },
+
+  _onMessageToChrome(e) {
+    // If target is window then we want the document principal, otherwise fallback to target itself.
+    let principal = e.target.nodePrincipal ? e.target.nodePrincipal : e.target.document.nodePrincipal;
+
+    if (e.detail) {
+      if (typeof e.detail != 'string') {
+        // Check if the principal is one of the ones that's allowed to send
+        // non-string values for e.detail.
+        let objectsAllowed = this._getWhitelistedPrincipals().some(whitelisted =>
+          principal.originNoSuffix == whitelisted.originNoSuffix);
+        if (!objectsAllowed) {
+          Cu.reportError("WebChannelMessageToChrome sent with an object from a non-whitelisted principal");
+          return;
+        }
+      }
+      sendAsyncMessage("WebChannelMessageToChrome", e.detail, { eventTarget: e.target }, principal);
+    } else  {
+      Cu.reportError("WebChannel message failed. No message detail.");
+    }
   }
-}, true, true);
+};
+
+WebChannelMessageToChromeListener.init();
 
 // This should be kept in sync with /browser/base/content.js.
 // Add message listener for "WebChannelMessageToContent" messages from chrome scripts.
 addMessageListener("WebChannelMessageToContent", function (e) {
   if (e.data) {
     // e.objects.eventTarget will be defined if sending a response to
     // a WebChannelMessageToChrome event. An unsolicited send
     // may not have an eventTarget defined, in this case send to the
--- a/toolkit/modules/WebChannel.jsm
+++ b/toolkit/modules/WebChannel.jsm
@@ -66,16 +66,25 @@ var WebChannelBroker = Object.create({
    */
   _listener: function (event) {
     let data = event.data;
     let sendingContext = {
       browser: event.target,
       eventTarget: event.objects.eventTarget,
       principal: event.principal,
     };
+    // data must be a string except for a few legacy origins allowed by browser-content.js.
+    if (typeof data == "string") {
+      try {
+        data = JSON.parse(data);
+      } catch (e) {
+        Cu.reportError("Failed to parse WebChannel data as a JSON object");
+        return;
+      }
+    }
 
     if (data && data.id) {
       if (!event.principal) {
         this._sendErrorEventToContent(data.id, sendingContext, "Message principal missing");
       } else {
         let validChannelFound = false;
         data.message = data.message || {};