dollarshaveclub/postmate

Feature request: origin whitelist for child

mrcoles opened this issue · 3 comments

What Is the issue?

I would love for the child to be able to set an origin whitelist, which—if set—means it will only handshake with origins that are in its whitelist.

Provide issue context below using code examples, images, or links

It could look something like this:

new Postmate.Model({
  height: () => document.height || document.body.offsetHeight
}, {
  originWhitelist: ['http://localhost:1234', 'https://example.com']
});

and then inside Postmate.Model.sendHandshakeReply (assuming the constructor sets the 2nd arg as this.opts) it could do something like this:

        if (e.data.postmate === 'handshake') {
          this.child.removeEventListener('message', shake, false);

          // whitelist check
          if (this.opts.originWhitelist && this.opts.originWhitelist.length) {
            if (!this.opts.originWhitelist.find(origin => sanitize(e, origin))) {
              return reject(`Invalid origin: ${e.origin}`);
            }
          }

          // … rest of function…
        }

I can always do this extra check post-handshake, but it’s more secure to do it pre-handshake. By checking pre-handshake, the child never sends a postMessage to untrusted parent origins (post-handshake it happens after). This prevents non-trusted origins from sniffing if there was a child accepting handshakes or not—which is relevant if the child page is behind a login or if it is a Chrome extension.

Here’s a wrapper for Postmate.Child that does the check post-handshake (but my snippet in the issue description would do the pre-handshake check):

import Postmate from "postmate";

class OriginChecker {
  constructor(originWhitelist) {
    this.originWhitelist = originWhitelist;
    this.isValid = true;
  }

  validate(parentOrigin) {
    if (this.originWhitelist.indexOf(parentOrigin) === -1) {
      this.isValid = false;
    }
    return this.isValid;
  }

  wrapModel(model) {
    const ret = {};
    Object.entries(model).forEach(([key, val]) => {
      ret[key] = data => {
        if (this.isValid) {
          return typeof val === "function" ? val(data) : val;
        } else {
          throw new Error("Invalid origin");
        }
      };
    });
    return ret;
  }
}

const makePostmateModel = (model, originWhitelist) => {
  const originChecker = new OriginChecker(originWhitelist);

  const handshake = new Postmate.Model(originChecker.wrapModel(model)).then(
    parent => {
      if (!originChecker.validate(parent.parentOrigin)) {
        const err = new Error(`Invalid parent origin: ${parent.parentOrigin}`);
        err.name = "InvalidParentOriginError";
        throw err;
      }

      return parent;
    }
  );

  return handshake;
};

export default makePostmateModel;

This is something Postmate should definitely solve. Can we assume that the iFrame provided will have a src of the origin we expect? It'd be nice to automate this and not have to provide any additional config.

I have the same question, imaging that there's anther parent origin,use the same way to connect to my child site, he can read all the data that the child set, and call all the functions the child site declared. This may cause secure problems.