Skip to content

Commit

Permalink
Merge pull request #605 from benjamind/delarre/origin-filtering
Browse files Browse the repository at this point in the history
Add origin filtering for expose
  • Loading branch information
surma authored Jan 24, 2023
2 parents f94aa8a + a086dfd commit e763a65
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 1 deletion.
25 changes: 24 additions & 1 deletion src/comlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,34 @@ export const transferHandlers = new Map<
["throw", throwTransferHandler],
]);

export function expose(obj: any, ep: Endpoint = self as any) {
function isAllowedOrigin(
allowedOrigins: (string | RegExp)[],
origin: string
): boolean {
for (const allowedOrigin of allowedOrigins) {
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,
allowedOrigins: (string | RegExp)[] = ["*"]
) {
ep.addEventListener("message", function callback(ev: MessageEvent) {
if (!ev || !ev.data) {
return;
}
if (!isAllowedOrigin(allowedOrigins, ev.origin)) {
console.warn(`Invalid origin '${ev.origin}' for comlink proxy`);
return;
}
const { id, type, path } = {
path: [] as string[],
...(ev.data as Message),
Expand Down
83 changes: 83 additions & 0 deletions tests/cross-origin.comlink.test.js
Original file line number Diff line number Diff line change
@@ -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 failed, the prototype was not updated
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, the prototype was updated
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();
});
});
21 changes: 21 additions & 0 deletions tests/fixtures/attack-iframe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<html>
<body>
<script>
window.addEventListener("message", (ev) => {
if (ev.data === "start") {
// send back a message to modify the prototype
parent.postMessage(
{
type: "SET",
value: { type: "RAW", value: "x" },
path: ["__proto__", "foo"],
},
"*"
);
parent.postMessage("done", "*");
}
});
parent.postMessage("ready", "*");
</script>
</body>
</html>

0 comments on commit e763a65

Please sign in to comment.