Improving security: only iframes opened with "openWebsiteAllowApi" property are now able to send/receive messages.

This commit is contained in:
David Négrier 2021-03-06 16:00:07 +01:00
parent e927e0fa16
commit 7d67f55012
4 changed files with 50 additions and 18 deletions

View file

@ -7,19 +7,29 @@ import {UserInputChatEvent} from "./Events/UserInputChatEvent";
/** /**
* Listens to messages from iframes and turn those messages into easy to use observables. * Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes.
*/ */
class IframeListener { class IframeListener {
private readonly _chatStream: Subject<ChatEvent> = new Subject(); private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable(); public readonly chatStream = this._chatStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>();
init() { init() {
window.addEventListener("message", (message) => { window.addEventListener("message", (message) => {
// Do we trust the sender of this message? // Do we trust the sender of this message?
//if (message.origin !== "http://example.com:8080") // Let's only accept messages from the iframe that are allowed.
// return; // Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let found = false;
// message.source is window.opener for (const iframe of this.iframes) {
// message.data is the data sent by the iframe if (iframe.contentWindow === message.source) {
found = true;
break;
}
}
if (!found) {
return;
}
const payload = message.data; const payload = message.data;
if (isIframeEventWrapper(payload)) { if (isIframeEventWrapper(payload)) {
@ -31,7 +41,17 @@ class IframeListener {
}, false); }, 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) { 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) { private postMessage(message: IframeEvent) {
// TODO: not the most effecient implementation if there are many events sent! for (const iframe of this.iframes) {
for (const iframe of document.querySelectorAll<HTMLIFrameElement>('iframe')) {
iframe.contentWindow?.postMessage(message, '*'); iframe.contentWindow?.postMessage(message, '*');
} }
} }

View file

@ -654,7 +654,7 @@ export class GameScene extends ResizableScene implements CenterListener {
coWebsiteManager.closeCoWebsite(); coWebsiteManager.closeCoWebsite();
}else{ }else{
const openWebsiteFunction = () => { 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); layoutManager.removeActionButton('openWebsite', this.userInputManager);
}; };

View file

@ -1,4 +1,5 @@
import {HtmlUtils} from "./HtmlUtils"; import {HtmlUtils} from "./HtmlUtils";
import {iframeListener} from "../Api/IframeListener";
export type CoWebsiteStateChangedCallback = () => void; 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. const animationTime = 500; //time used by the css transitions, in ms.
class CoWebsiteManager { class CoWebsiteManager {
private opened: iframeStates = iframeStates.closed; private opened: iframeStates = iframeStates.closed;
private observers = new Array<CoWebsiteStateChangedCallback>(); private observers = new Array<CoWebsiteStateChangedCallback>();
/** /**
@ -21,12 +22,12 @@ class CoWebsiteManager {
* So we use this promise to queue up every cowebsite state transition * So we use this promise to queue up every cowebsite state transition
*/ */
private currentOperationPromise: Promise<void> = Promise.resolve(); private currentOperationPromise: Promise<void> = Promise.resolve();
private cowebsiteDiv: HTMLDivElement; private cowebsiteDiv: HTMLDivElement;
constructor() { constructor() {
this.cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId); this.cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId);
} }
private close(): void { private close(): void {
this.cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition this.cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition
this.cowebsiteDiv.classList.add('hidden'); this.cowebsiteDiv.classList.add('hidden');
@ -42,7 +43,7 @@ class CoWebsiteManager {
this.opened = iframeStates.opened; 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.load();
this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close"> this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg"> <img src="resources/logos/close.svg">
@ -57,11 +58,14 @@ class CoWebsiteManager {
iframe.id = 'cowebsite-iframe'; iframe.id = 'cowebsite-iframe';
iframe.src = (new URL(url, base)).toString(); iframe.src = (new URL(url, base)).toString();
if (allowPolicy) { if (allowPolicy) {
iframe.allow = allowPolicy; iframe.allow = allowPolicy;
} }
const onloadPromise = new Promise((resolve) => { const onloadPromise = new Promise((resolve) => {
iframe.onload = () => resolve(); iframe.onload = () => resolve();
}); });
if (allowApi) {
iframeListener.registerIframe(iframe);
}
this.cowebsiteDiv.appendChild(iframe); this.cowebsiteDiv.appendChild(iframe);
const onTimeoutPromise = new Promise((resolve) => { const onTimeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve(), 2000); setTimeout(() => resolve(), 2000);
@ -92,6 +96,10 @@ class CoWebsiteManager {
if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example
this.close(); this.close();
this.fire(); this.fire();
const iframe = this.cowebsiteDiv.querySelector('iframe');
if (iframe) {
iframeListener.unregisterIframe(iframe);
}
setTimeout(() => { setTimeout(() => {
this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close"> this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg"> <img src="resources/logos/close.svg">
@ -122,7 +130,7 @@ class CoWebsiteManager {
} }
} }
//todo: is it still useful to allow any kind of observers? //todo: is it still useful to allow any kind of observers?
public onStateChange(observer: CoWebsiteStateChangedCallback) { public onStateChange(observer: CoWebsiteStateChangedCallback) {
this.observers.push(observer); this.observers.push(observer);
} }
@ -134,4 +142,4 @@ class CoWebsiteManager {
} }
} }
export const coWebsiteManager = new CoWebsiteManager(); export const coWebsiteManager = new CoWebsiteManager();

View file

@ -44,6 +44,11 @@
"name":"openWebsite", "name":"openWebsite",
"type":"string", "type":"string",
"value":"iframe.html" "value":"iframe.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}], }],
"type":"tilelayer", "type":"tilelayer",
"visible":true, "visible":true,