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.