28

Window.postMessage() has a targetOrigin parameter that can be set to a URI (to ensure the message reaches only a specific url). It can also be set to * of course (not recommended), but is there a way to specify multiple URIs as allowed?

At present I'm simply firing off one postMessage() call for each domain, but this seems a bit hacky to say the least.

Michael Berry
  • 70,193
  • 21
  • 157
  • 216
  • http://www.mograblog.com/2013/02/how-to-make-post-message-support-multi-origins-and-multi-messages.html – Marvin Feb 25 '20 at 23:09

3 Answers3

25

Unfortunately you can't. You should either provide "*" or single specified domain.

Kamil
  • 424
  • 4
  • 8
  • Thanks for the clarification. I suspected this might be the case but wanted to make sure. – Michael Berry Mar 11 '16 at 12:21
  • 1
    Keep in mind that if you are sending valuable data such as auth tokens via postMessage using `"*"`, it is possible for an attacker to open your page and have the data get sent to them. – Jespertheend Aug 04 '21 at 12:24
18

You could try to send multiples time, one per domain:

targetWindow.postMessage(message, "https://domain1.com");
targetWindow.postMessage(message, "http://localhost");

⚠ It is not recommended use "*" to prevent security vulnerabilities.

You can also make an array + loop

Juanmabs22
  • 1,194
  • 9
  • 10
  • `postMessage` will throw a `DOMException` if the `targetOrigin` parameter doesn't exactly match `targetWindow.origin` btw, so each call to `postMessage` needs to be wrapped in `try/catch`, otherwise only the first origin (`https://domain1.com`) will be attempted. – Dai Aug 18 '23 at 08:59
2

While Juanmabs22's answer should work™, this will also trigger an uncatchable console error message per non-matching origin, which may not be optimal for the ones who like their website to produce no error message at all.

For the ones who still want a secured communication channel (and thus don't want the unsafe wildcard solution), there is an, admittedly cumbersome, workaround:
Before sending the sensitive data, you can set up an handshake negotiation initiated through the unsafe targetOrigin: "*". In this first message you'll ask for the embedder's origin, and then, only if this origin matches your expected ones, you'll start a secured communication using that origin. This means that the embedder has to expose their origin, but they can check that your message comes from your script's origin using the MessageEvent#origin property.

So this would give,

In embeddee.html (your script):

async function initComm(allowedOrigins = []){
  const unsafe = new MessageChannel();
  parent.postMessage("negotiation", {
    targetOrigin: "*", // we're using the unsafe wildcard only for this request
    transfer: [unsafe.port1]
  });
  // In case we're talking to someone who doesn't know about our handshake
  const timeout = AbortSignal.timeout(100);
  const targetOrigin = await Promise.race([
    new Promise((res, rej) => timeout.onabort = (evt) => {
      unsafe.port2.close(); // clean
      rej(timeout.reason);
    }),
    // Wait for a response
    new Promise((res) => unsafe.port2.onmessage = ({data}) => res(data))
  ]);
  unsafe.port2.close();
  if (!allowedOrigins.includes(targetOrigin)) {
    throw new Error("Unknown origin: " + targetOrigin);
  }
  const safe = new MessageChannel();
  // Even if they lied, we're using the safe targetOrigin option
  parent.postMessage("begin", { targetOrigin, transfer: [safe.port1] });
  return safe.port2;
}
const safePort = await initComm(["https://embedder-1.com", "https://another-origin.com"]);
// now you can use this MessageChannel to send sensitive data
safePort.postMessage("Here is a secret");

And on the embedder's side:

onmessage = ({origin, data, ports}) => {
  if (origin === YOUR_SCRIPT_ORIGIN) { // only if they recognize you
    switch(data) {
      case "negotiation":
      ports[0].postMessage(window.origin);
      break;
    case "begin":
      beginComm(ports[0]);
      break;
    }
  }
}
function beginComm(safePort) {
  safePort.onmessage = ({data}) => {
    // Now they can handle your messages
    console.log("Safely received a new message", data);
  };
}

Unfortunately, StackSnippet's null origined iframes make for a very bad example, so here is an outsourced JSFiddle.

Kaiido
  • 123,334
  • 13
  • 219
  • 285