Bug 1337743 - More data validation checks for Marionette messages; r?whimboo draft
authorAndreas Tolfsen <ato@mozilla.com>
Thu, 09 Feb 2017 18:22:09 +0000
changeset 551795 29f3ebd212baa90eb75e0fb6b1af27711c32c99c
parent 551794 a6889517f2174010336e20c0416c6598dddbd4e2
child 551796 c955a1221f7ffe6bcec52928b38dec5031fc1f6c
push id51152
push userbmo:ato@mozilla.com
push dateMon, 27 Mar 2017 12:36:48 +0000
reviewerswhimboo
bugs1337743
milestone55.0a1
Bug 1337743 - More data validation checks for Marionette messages; r?whimboo This change introduces more data validation checks on unmarshaling Marionette protocol messages. Specifically, validation of message.Command's and message.Response's constructor arguments and packet contents in their respective fromMsg functions are tested. Doing these tests ensures more safety further down the pipeline with respect to the data integrity in Marionette commands. MozReview-Commit-ID: BxYipX5zfo9
testing/marionette/message.js
testing/marionette/test_message.js
--- a/testing/marionette/message.js
+++ b/testing/marionette/message.js
@@ -4,16 +4,17 @@
 
 "use strict";
 
 var {utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
+Cu.import("chrome://marionette/content/assert.js");
 Cu.import("chrome://marionette/content/error.js");
 
 this.EXPORTED_SYMBOLS = [
   "Command",
   "Message",
   "MessageOrigin",
   "Response",
 ];
@@ -62,28 +63,28 @@ Message.fromMsg = function (data) {
  * The command can be synthesised from the message passed over the
  * Marionette socket using the {@code fromMsg} function.  The format of
  * a message is:
  *
  *     [type, id, name, params]
  *
  * where
  *
- *   type:
+ *   type (integer)
  *     Must be zero (integer). Zero means that this message is a command.
  *
- *   id:
- *     Number used as a sequence number.  The server replies with a
- *     requested id.
+ *   id (integer)
+ *     Integer used as a sequence number.  The server replies with the
+ *     same ID for the response.
  *
- *   name:
+ *   name (string)
  *     String representing the command name with an associated set of
  *     remote end steps.
  *
- *   params:
+ *   params (JSON Object or null)
  *     Object of command function arguments.  The keys of this object
  *     must be strings, but the values can be arbitrary values.
  *
  * A command has an associated message {@code id} that prevents the
  * dispatcher from sending responses in the wrong order.
  *
  * The command may also have optional error- and result handlers that
  * are called when the client returns with a response.  These are
@@ -94,19 +95,19 @@ Message.fromMsg = function (data) {
  *     Message ID unique identifying this message.
  * @param {string} name
  *     Command name.
  * @param {Object<string, ?>} params
  *     Command parameters.
  */
 this.Command = class {
   constructor(msgID, name, params = {}) {
-    this.id = msgID;
-    this.name = name;
-    this.parameters = params;
+    this.id = assert.integer(msgID);
+    this.name = assert.string(name);
+    this.parameters = assert.object(params);
 
     this.onerror = null;
     this.onresult = null;
 
     this.origin = MessageOrigin.Client;
     this.sent = false;
   }
 
@@ -114,35 +115,36 @@ this.Command = class {
    * Calls the error- or result handler associated with this command.
    * This function can be replaced with a custom response handler.
    *
    * @param {Response} resp
    *     The response to pass on to the result or error to the
    *     {@code onerror} or {@code onresult} handlers to.
    */
   onresponse(resp) {
-    if (resp.error && this.onerror) {
+    if (this.onerror && resp.error) {
       this.onerror(resp.error);
-    } else if (resp.body && this.onresult) {
+    } else if (this.onresult && resp.body) {
       this.onresult(resp.body);
     }
   }
 
   toMsg() {
     return [Command.TYPE, this.id, this.name, this.parameters];
   }
 
   toString() {
     return "Command {id: " + this.id + ", " +
         "name: " + JSON.stringify(this.name) + ", " +
         "parameters: " + JSON.stringify(this.parameters) + "}";
   }
 
   static fromMsg(msg) {
-    let [msgID, name, params] = [msg[1], msg[2], msg[3]];
+    let [type, msgID, name, params] = msg;
+    assert.that(n => n === Command.TYPE)(type);
 
     // if parameters are given but null, treat them as undefined
     if (params === null) {
       params = undefined;
     }
 
     return new Command(msgID, name, params);
   }
@@ -196,33 +198,32 @@ this.ResponseBody = () => new Proxy({}, 
  * modification through the available setters.  To send data in a response,
  * you modify the body property on the response.  The body property can
  * also be replaced completely.
  *
  * The response is sent implicitly by CommandProcessor when a command
  * has finished executing, and any modifications made subsequent to that
  * will have no effect.
  *
- * @param {number} msgId
+ * @param {number} msgID
  *     Message ID tied to the corresponding command request this is a
  *     response for.
  * @param {function(Response|Message)} respHandler
  *     Function callback called on sending the response.
  */
 this.Response = class {
-  constructor(msgId, respHandler) {
-    this.id = msgId;
+  constructor(msgID, respHandler = () => {}) {
+    this.id = assert.integer(msgID);
+    this.respHandler_ = assert.callable(respHandler);
 
     this.error = null;
     this.body = ResponseBody();
 
     this.origin = MessageOrigin.Server;
     this.sent = false;
-
-    this.respHandler_ = respHandler;
   }
 
   /**
    * Sends response conditionally, given a predicate.
    *
    * @param {function(Response): boolean} predicate
    *     A predicate taking a Response object and returning a boolean.
    */
@@ -277,16 +278,20 @@ this.Response = class {
 
   toString() {
     return "Response {id: " + this.id + ", " +
         "error: " + JSON.stringify(this.error) + ", " +
         "body: " + JSON.stringify(this.body) + "}";
   }
 
   static fromMsg(msg) {
-    let resp = new Response(msg[1], null);
-    resp.error = msg[2];
-    resp.body = msg[3];
+    let [type, msgID, err, body] = msg;
+    assert.that(n => n === Response.TYPE)(type);
+
+    let resp = new Response(msgID);
+    resp.error = assert.string(err);
+
+    resp.body = body;
     return resp;
   }
 };
 
 Response.TYPE = 1;
--- a/testing/marionette/test_message.js
+++ b/testing/marionette/test_message.js
@@ -12,16 +12,17 @@ add_test(function test_MessageOrigin() {
   equal(1, MessageOrigin.Server);
 
   run_next_test();
 });
 
 add_test(function test_Message_fromMsg() {
   let cmd = new Command(4, "foo");
   let resp = new Response(5, () => {});
+  resp.error = "foo";
 
   ok(Message.fromMsg(cmd.toMsg()) instanceof Command);
   ok(Message.fromMsg(resp.toMsg()) instanceof Response);
   Assert.throws(() => Message.fromMsg([3, 4, 5, 6]),
       /Unrecognised message type in packet/);
 
   run_next_test();
 });
@@ -38,37 +39,37 @@ add_test(function test_Command() {
 
   run_next_test();
 });
 
 add_test(function test_Command_onresponse() {
   let onerrorOk = false;
   let onresultOk = false;
 
-  let cmd = new Command();
+  let cmd = new Command(7, "foo");
   cmd.onerror = () => onerrorOk = true;
   cmd.onresult = () => onresultOk = true;
 
-  let errorResp = new Response();
+  let errorResp = new Response(8, () => {});
   errorResp.error = new WebDriverError("foo");
 
-  let bodyResp = new Response();
+  let bodyResp = new Response(9, () => {});
   bodyResp.body = "bar";
 
   cmd.onresponse(errorResp);
   equal(true, onerrorOk);
   equal(false, onresultOk);
 
   cmd.onresponse(bodyResp);
   equal(true, onresultOk);
 
   run_next_test();
 });
 
-add_test(function test_Command_fromMsg() {
+add_test(function test_Command_ctor() {
   let cmd = new Command(42, "bar", {bar: "baz"});
   let msg = cmd.toMsg();
 
   equal(Command.TYPE, msg[0]);
   equal(cmd.id, msg[1]);
   equal(cmd.name, msg[2]);
   equal(cmd.parameters, msg[3]);
 
@@ -90,25 +91,34 @@ add_test(function test_Command_fromMsg()
 
   let msg = c1.toMsg();
   let c2 = Command.fromMsg(msg);
 
   equal(c1.id, c2.id);
   equal(c1.name, c2.name);
   equal(c1.parameters, c2.parameters);
 
+  Assert.throws(() => Command.fromMsg([null, 2, "foo", {}]));
+  Assert.throws(() => Command.fromMsg([1, 2, "foo", {}]));
+  Assert.throws(() => Command.fromMsg([0, null, "foo", {}]));
+  Assert.throws(() => Command.fromMsg([0, 2, null, {}]));
+  Assert.throws(() => Command.fromMsg([0, 2, "foo", false]));
+
+  let nullParams = Command.fromMsg([0, 2, "foo", null]);
+  equal("[object Object]", Object.prototype.toString.call(nullParams.parameters));
+
   run_next_test();
 });
 
 add_test(function test_Command_TYPE() {
   equal(0, Command.TYPE);
   run_next_test();
 });
 
-add_test(function test_Response() {
+add_test(function test_Response_ctor() {
   let handler = () => run_next_test();
 
   let resp = new Response(42, handler);
   equal(42, resp.id);
   equal(null, resp.error);
   ok("origin" in resp);
   equal(MessageOrigin.Server, resp.origin);
   equal(false, resp.sent);
@@ -154,51 +164,57 @@ add_test(function test_Response_sendErro
 
   resp.sent = false;
   Assert.throws(() => resp.sendError(new Error()));
 
   run_next_test();
 });
 
 add_test(function test_Response_toMsg() {
-  let resp = new Response(42);
+  let resp = new Response(42, () => {});
   let msg = resp.toMsg();
 
   equal(Response.TYPE, msg[0]);
   equal(resp.id, msg[1]);
   equal(resp.error, msg[2]);
   equal(resp.body, msg[3]);
 
   run_next_test();
 });
 
 add_test(function test_Response_toString() {
-  let resp = new Response(42);
+  let resp = new Response(42, () => {});
   resp.error = "foo";
   resp.body = "bar";
 
   equal(`Response {id: ${resp.id}, ` +
       `error: ${JSON.stringify(resp.error)}, ` +
       `body: ${JSON.stringify(resp.body)}}`,
       resp.toString());
 
   run_next_test();
 });
 
 add_test(function test_Response_fromMsg() {
-  let r1 = new Response(42);
+  let r1 = new Response(42, () => {});
   r1.error = "foo";
   r1.body = "bar";
 
   let msg = r1.toMsg();
   let r2 = Response.fromMsg(msg);
 
   equal(r1.id, r2.id);
   equal(r1.error, r2.error);
   equal(r1.body, r2.body);
 
+  Assert.throws(() => Response.fromMsg([null, 2, "foo", {}]));
+  Assert.throws(() => Response.fromMsg([0, 2, "foo", {}]));
+  Assert.throws(() => Response.fromMsg([1, null, "foo", {}]));
+  Assert.throws(() => Response.fromMsg([1, 2, null, {}]));
+  Response.fromMsg([1, 2, "foo", null]);
+
   run_next_test();
 });
 
 add_test(function test_Response_TYPE() {
   equal(1, Response.TYPE);
   run_next_test();
 });