From 7d67f5501207f02629ba1dfbcbf2de2905584148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Sat, 6 Mar 2021 16:00:07 +0100 Subject: [PATCH] Improving security: only iframes opened with "openWebsiteAllowApi" property are now able to send/receive messages. --- front/src/Api/IframeListener.ts | 35 +++++++++++++++++++++------- front/src/Phaser/Game/GameScene.ts | 2 +- front/src/WebRtc/CoWebsiteManager.ts | 26 ++++++++++++++------- maps/tests/iframe_api.json | 5 ++++ 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index a94d294c..e91b92f3 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -7,19 +7,29 @@ import {UserInputChatEvent} from "./Events/UserInputChatEvent"; /** * Listens to messages from iframes and turn those messages into easy to use observables. + * Also allows to send messages to those iframes. */ class IframeListener { private readonly _chatStream: Subject = new Subject(); public readonly chatStream = this._chatStream.asObservable(); + private readonly iframes = new Set(); + init() { window.addEventListener("message", (message) => { // Do we trust the sender of this message? - //if (message.origin !== "http://example.com:8080") - // return; - - // message.source is window.opener - // message.data is the data sent by the iframe + // Let's only accept messages from the iframe that are allowed. + // Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain). + let found = false; + for (const iframe of this.iframes) { + if (iframe.contentWindow === message.source) { + found = true; + break; + } + } + if (!found) { + return; + } const payload = message.data; if (isIframeEventWrapper(payload)) { @@ -31,7 +41,17 @@ class IframeListener { }, false); + } + /** + * Allows the passed iFrame to send/receive messages via the API. + */ + registerIframe(iframe: HTMLIFrameElement): void { + this.iframes.add(iframe); + } + + unregisterIframe(iframe: HTMLIFrameElement): void { + this.iframes.delete(iframe); } sendUserInputChat(message: string) { @@ -44,11 +64,10 @@ class IframeListener { } /** - * Sends the message... to absolutely all the iFrames that can be found in the current document. + * Sends the message... to all allowed iframes. */ private postMessage(message: IframeEvent) { - // TODO: not the most effecient implementation if there are many events sent! - for (const iframe of document.querySelectorAll('iframe')) { + for (const iframe of this.iframes) { iframe.contentWindow?.postMessage(message, '*'); } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 2561a636..5ca3c749 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -654,7 +654,7 @@ export class GameScene extends ResizableScene implements CenterListener { coWebsiteManager.closeCoWebsite(); }else{ const openWebsiteFunction = () => { - coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsitePolicy') as string | undefined); + coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined); layoutManager.removeActionButton('openWebsite', this.userInputManager); }; diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 4e74c4a7..472e7a13 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -1,4 +1,5 @@ import {HtmlUtils} from "./HtmlUtils"; +import {iframeListener} from "../Api/IframeListener"; export type CoWebsiteStateChangedCallback = () => void; @@ -12,8 +13,8 @@ const cowebsiteDivId = "cowebsite"; // the id of the parent div of the iframe. const animationTime = 500; //time used by the css transitions, in ms. class CoWebsiteManager { - - private opened: iframeStates = iframeStates.closed; + + private opened: iframeStates = iframeStates.closed; private observers = new Array(); /** @@ -21,12 +22,12 @@ class CoWebsiteManager { * So we use this promise to queue up every cowebsite state transition */ private currentOperationPromise: Promise = Promise.resolve(); - private cowebsiteDiv: HTMLDivElement; - + private cowebsiteDiv: HTMLDivElement; + constructor() { this.cowebsiteDiv = HtmlUtils.getElementByIdOrFail(cowebsiteDivId); } - + private close(): void { this.cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition this.cowebsiteDiv.classList.add('hidden'); @@ -42,7 +43,7 @@ class CoWebsiteManager { this.opened = iframeStates.opened; } - public loadCoWebsite(url: string, base: string, allowPolicy?: string): void { + public loadCoWebsite(url: string, base: string, allowApi?: boolean, allowPolicy?: string): void { this.load(); this.cowebsiteDiv.innerHTML = `