From f1407de098e7448895c3548aa4a67901fd5cad61 Mon Sep 17 00:00:00 2001 From: Ben Delarre Date: Thu, 19 Jan 2023 15:54:48 -0800 Subject: [PATCH 1/4] Add origin filtering for expose --- src/comlink.ts | 25 ++++++++- tests/cross-origin.comlink.test.js | 83 ++++++++++++++++++++++++++++++ tests/fixtures/attack-iframe.html | 21 ++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 tests/cross-origin.comlink.test.js create mode 100644 tests/fixtures/attack-iframe.html diff --git a/src/comlink.ts b/src/comlink.ts index 2da1643d..fc79ea7b 100644 --- a/src/comlink.ts +++ b/src/comlink.ts @@ -281,11 +281,34 @@ export const transferHandlers = new Map< ["throw", throwTransferHandler], ]); -export function expose(obj: any, ep: Endpoint = self as any) { +function isAllowedOrigin( + origins: (string | RegExp)[], + origin: string +): boolean { + for (const allowedOrigin of origins) { + if (origin === allowedOrigin || allowedOrigin === "*") { + return true; + } + if (allowedOrigin instanceof RegExp && allowedOrigin.test(origin)) { + return true; + } + } + return false; +} + +export function expose( + obj: any, + ep: Endpoint = self as any, + origins: (string | RegExp)[] = ["*"] +) { ep.addEventListener("message", function callback(ev: MessageEvent) { if (!ev || !ev.data) { return; } + if (!isAllowedOrigin(origins, ev.origin)) { + console.warn(`Invalid origin '${ev.origin}' for comlink proxy`); + return; + } const { id, type, path } = { path: [] as string[], ...(ev.data as Message), diff --git a/tests/cross-origin.comlink.test.js b/tests/cross-origin.comlink.test.js new file mode 100644 index 00000000..2284d8fa --- /dev/null +++ b/tests/cross-origin.comlink.test.js @@ -0,0 +1,83 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as Comlink from "/base/dist/esm/comlink.mjs"; + +describe("Comlink origin filtering", function () { + it("rejects messages from unknown origin", async function () { + // expose on our window so comlink is listening to window postmessage + const obj = { my: "value" }; + Comlink.expose(obj, self, [/^http:\/\/localhost(:[0-9]+)?\/?$/]); + + let handler; + // juggle async timings to get the attack started + const attackComplete = new Promise((resolve, reject) => { + handler = (ev) => { + if (ev.data === "ready" && ev.origin === "null") { + // tell the iframe it can start the attack + ifr.contentWindow.postMessage("start", "*"); + } else if (ev.data === "done") { + // confirm the attack succeeded + expect(Object.prototype.foo).to.be.undefined; + expect(obj.my).to.equal("value"); + resolve(); + } + }; + window.addEventListener("message", handler); + }); + // create a sandboxed iframe for the attack + const ifr = document.createElement("iframe"); + ifr.sandbox.add("allow-scripts"); + ifr.src = "/base/tests/fixtures/attack-iframe.html"; + document.body.appendChild(ifr); + // wait for the iframe to load + await new Promise((resolve) => (ifr.onload = resolve)); + // and wait for the attack to complete + await attackComplete; + window.removeEventListener("message", handler); + ifr.remove(); + }); + it("accepts messages from matching origin", async function () { + // expose on our window so comlink is listening to window postmessage + const obj = { my: "value" }; + Comlink.expose(obj, self, [/^http:\/\/localhost(:[0-9]+)?\/?$/]); + + let handler; + // juggle async timings to get the attack started + const attackComplete = new Promise((resolve, reject) => { + handler = (ev) => { + if (ev.data === "ready" && ev.origin === window.origin) { + // tell the iframe it can start the attack + ifr.contentWindow.postMessage("start", "*"); + } else if (ev.data === "done") { + // confirm the attack succeeded + expect(Object.prototype.foo).to.equal("x"); + expect(obj.my).to.equal("value"); + resolve(); + } + }; + window.addEventListener("message", handler); + }); + // create a sandboxed iframe for the attack, but with same origin + const ifr = document.createElement("iframe"); + ifr.sandbox.add("allow-scripts", "allow-same-origin"); + ifr.src = "/base/tests/fixtures/attack-iframe.html"; + document.body.appendChild(ifr); + // wait for the iframe to load + await new Promise((resolve) => (ifr.onload = resolve)); + // and wait for the attack to complete + await attackComplete; + window.removeEventListener("message", handler); + ifr.remove(); + }); +}); diff --git a/tests/fixtures/attack-iframe.html b/tests/fixtures/attack-iframe.html new file mode 100644 index 00000000..e32c951b --- /dev/null +++ b/tests/fixtures/attack-iframe.html @@ -0,0 +1,21 @@ + + + + + From 9338813941c661a235a11637b43d156507d64aed Mon Sep 17 00:00:00 2001 From: Ben Delarre Date: Thu, 19 Jan 2023 16:03:51 -0800 Subject: [PATCH 2/4] Fixes comments --- tests/cross-origin.comlink.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cross-origin.comlink.test.js b/tests/cross-origin.comlink.test.js index 2284d8fa..c72a9469 100644 --- a/tests/cross-origin.comlink.test.js +++ b/tests/cross-origin.comlink.test.js @@ -27,7 +27,7 @@ describe("Comlink origin filtering", function () { // tell the iframe it can start the attack ifr.contentWindow.postMessage("start", "*"); } else if (ev.data === "done") { - // confirm the attack succeeded + // confirm the attack failed, the prototype was not updated expect(Object.prototype.foo).to.be.undefined; expect(obj.my).to.equal("value"); resolve(); @@ -60,7 +60,7 @@ describe("Comlink origin filtering", function () { // tell the iframe it can start the attack ifr.contentWindow.postMessage("start", "*"); } else if (ev.data === "done") { - // confirm the attack succeeded + // confirm the attack succeeded, the prototype was updated expect(Object.prototype.foo).to.equal("x"); expect(obj.my).to.equal("value"); resolve(); From b4d420d83bf021ac2d947307027de7609a4b2f4f Mon Sep 17 00:00:00 2001 From: Ben Delarre Date: Mon, 23 Jan 2023 11:53:43 -0800 Subject: [PATCH 3/4] Update src/comlink.ts Co-authored-by: Surma --- src/comlink.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/comlink.ts b/src/comlink.ts index fc79ea7b..e712e13e 100644 --- a/src/comlink.ts +++ b/src/comlink.ts @@ -282,7 +282,7 @@ export const transferHandlers = new Map< ]); function isAllowedOrigin( - origins: (string | RegExp)[], + allowedOrigins: (string | RegExp)[], origin: string ): boolean { for (const allowedOrigin of origins) { From a086dfd9a9d047d48ba64d4411918bf8d8e8822b Mon Sep 17 00:00:00 2001 From: Ben Delarre Date: Mon, 23 Jan 2023 11:54:34 -0800 Subject: [PATCH 4/4] Updates argument naming --- src/comlink.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/comlink.ts b/src/comlink.ts index e712e13e..3e4b8a1e 100644 --- a/src/comlink.ts +++ b/src/comlink.ts @@ -285,7 +285,7 @@ function isAllowedOrigin( allowedOrigins: (string | RegExp)[], origin: string ): boolean { - for (const allowedOrigin of origins) { + for (const allowedOrigin of allowedOrigins) { if (origin === allowedOrigin || allowedOrigin === "*") { return true; } @@ -299,13 +299,13 @@ function isAllowedOrigin( export function expose( obj: any, ep: Endpoint = self as any, - origins: (string | RegExp)[] = ["*"] + allowedOrigins: (string | RegExp)[] = ["*"] ) { ep.addEventListener("message", function callback(ev: MessageEvent) { if (!ev || !ev.data) { return; } - if (!isAllowedOrigin(origins, ev.origin)) { + if (!isAllowedOrigin(allowedOrigins, ev.origin)) { console.warn(`Invalid origin '${ev.origin}' for comlink proxy`); return; }