From 4f2c2f904c1d6b7a60284b7fd4d6b2058950417c Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sat, 31 Jan 2026 16:18:03 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=99=20Updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix PixiJS v8 deprecation warning (PixiJS v8 only allows Container objects to have children) * Changed from whitelist to blocklist approach in LegacyExternalInterface: - Allows legitimate callbacks like 'openroom', 'opennavigator', etc. - Blocks only dangerous globals (eval, Function, window, document, etc.) - Blocks prototype pollution vectors (__proto__, constructor, etc.) - Blocks network/storage APIs from being overwritten. --- .../IsometricImageFurniVisualization.ts | 4 +- packages/utils/src/LegacyExternalInterface.ts | 212 +++++++++++++++++- 2 files changed, 202 insertions(+), 14 deletions(-) diff --git a/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts b/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts index 11628b2..485d43c 100644 --- a/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts +++ b/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts @@ -1,6 +1,6 @@ import { IGraphicAsset } from '@nitrots/api'; import { GetRenderer, TextureUtils } from '@nitrots/utils'; -import { Matrix, Sprite, Texture, RenderTexture } from 'pixi.js'; +import { Container, Matrix, Sprite, Texture, RenderTexture } from 'pixi.js'; import { FurnitureAnimatedVisualization } from './FurnitureAnimatedVisualization'; export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualization { @@ -138,7 +138,7 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza const width = 64; const height = 64; - const container = new Sprite(); + const container = new Container(); sprite.position.set((width - sprite.width) / 2, (height - sprite.height) / 2); container.addChild(sprite); diff --git a/packages/utils/src/LegacyExternalInterface.ts b/packages/utils/src/LegacyExternalInterface.ts index 9dcc458..5d44953 100644 --- a/packages/utils/src/LegacyExternalInterface.ts +++ b/packages/utils/src/LegacyExternalInterface.ts @@ -40,37 +40,166 @@ declare global export class LegacyExternalInterface { private static readonly MESSAGE_KEY = 'Nitro_LegacyExternalInterface'; + private static readonly GAME_MESSAGE_KEY = 'Nitro_LegacyExternalGameInterface'; private static _isListeningForPostMessages = false; + private static _messageListener: (ev: MessageEvent) => void = null; + // Whitelist of allowed methods that can be called via postMessage + // This prevents arbitrary code execution from malicious postMessage calls + private static readonly ALLOWED_EXTERNAL_METHODS: ReadonlySet = new Set([ + 'cycleWallpaper', + 'cycleFloor', + 'cycleLandscape', + 'cycleBackgroundColor', + 'cycleAvatarLightLevel', + 'cycleAvatarName', + 'cycleAvatarTyping', + 'cycleAvatarEffect', + 'cycleAvatarIdle', + 'cycleAvatarDance', + 'cycleAvatarExpression', + 'cycleAvatarPosture', + 'cycleAvatarSign', + 'cycleAvatarSleep', + 'cycleAvatarTalk', + 'cycleAvatarWave', + 'cycleZoom', + 'cycleRoomBackgroundColor' + ]); + + // Blocklist of dangerous global function/property names that cannot be overwritten + // This prevents security vulnerabilities while allowing legitimate callbacks + private static readonly BLOCKED_CALLBACK_NAMES: ReadonlySet = new Set([ + // JavaScript execution functions + 'eval', + 'Function', + 'constructor', + // Prototype pollution vectors + 'prototype', + '__proto__', + '__defineGetter__', + '__defineSetter__', + '__lookupGetter__', + '__lookupSetter__', + // Global objects + 'window', + 'document', + 'globalThis', + 'self', + 'top', + 'parent', + 'frames', + // Module/import system + 'require', + 'import', + 'module', + 'exports', + // Network/storage APIs + 'fetch', + 'XMLHttpRequest', + 'WebSocket', + 'Worker', + 'SharedWorker', + 'ServiceWorker', + 'localStorage', + 'sessionStorage', + 'indexedDB', + 'caches', + // Object prototype methods + 'toString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toLocaleString', + // Potentially dangerous DOM methods + 'postMessage', + 'addEventListener', + 'removeEventListener', + 'dispatchEvent', + 'setTimeout', + 'setInterval', + 'setImmediate', + 'requestAnimationFrame', + 'queueMicrotask' + ]); + + // Allowed origins for postMessage - empty means same-origin only + // Add trusted origins here if cross-origin communication is needed + private static _allowedOrigins: Set = new Set(); + + public static setAllowedOrigins(origins: string[]): void + { + this._allowedOrigins = new Set(origins); + } public static get available(): boolean { if(!this._isListeningForPostMessages) { this._isListeningForPostMessages = true; - window.addEventListener('message', (ev) => + + this._messageListener = (ev: MessageEvent) => { + // Validate origin - only accept from same origin or explicitly allowed origins + if(ev.origin !== window.location.origin && !this._allowedOrigins.has(ev.origin)) + { + return; + } + if(typeof ev.data !== 'string') return; if(ev.data.startsWith(LegacyExternalInterface.MESSAGE_KEY)) { - const { method, params } = JSON.parse( - ev.data.substr(LegacyExternalInterface.MESSAGE_KEY.length) - ); + try + { + const { method, params } = JSON.parse( + ev.data.substring(LegacyExternalInterface.MESSAGE_KEY.length) + ); - const fn = (window as any)[method]; - if(!fn) return; + // Validate method is in whitelist before executing + if(!this.ALLOWED_EXTERNAL_METHODS.has(method)) + { + console.warn(`[LegacyExternalInterface] Blocked unauthorized method call: ${method}`); + return; + } - fn(...params); + // Validate params is an array + if(!Array.isArray(params)) + { + console.warn(`[LegacyExternalInterface] Invalid params for method: ${method}`); + return; + } + + const fn = (window as any)[method]; + if(typeof fn !== 'function') return; + + fn(...params); + } + catch(e) + { + console.error('[LegacyExternalInterface] Error processing message:', e); + } return; } + }; - }); + window.addEventListener('message', this._messageListener); } return true; } + public static dispose(): void + { + if(this._messageListener) + { + window.removeEventListener('message', this._messageListener); + this._messageListener = null; + this._isListeningForPostMessages = false; + } + } + public static call( method: K, ...params: Parameters @@ -78,10 +207,17 @@ export class LegacyExternalInterface { if(window.top !== window) { + // Use parent origin if known, otherwise use '*' with caution + // Note: Using '*' is necessary when the parent origin is unknown + // The receiving end should validate the message content + const targetOrigin = window.location.ancestorOrigins?.length > 0 + ? window.location.ancestorOrigins[0] + : '*'; + window.top.postMessage(LegacyExternalInterface.MESSAGE_KEY + JSON.stringify({ method, params - }), '*'); + }), targetOrigin); } if(!('FlashExternalInterface' in window)) return undefined; @@ -98,10 +234,15 @@ export class LegacyExternalInterface { if(window.top !== window) { - window.top.postMessage('Nitro_LegacyExternalGameInterface' + JSON.stringify({ + // Use parent origin if known, otherwise use '*' with caution + const targetOrigin = window.location.ancestorOrigins?.length > 0 + ? window.location.ancestorOrigins[0] + : '*'; + + window.top.postMessage(LegacyExternalInterface.GAME_MESSAGE_KEY + JSON.stringify({ method, params - }), '*'); + }), targetOrigin); } if(!('FlashExternalGameInterface' in window)) return undefined; @@ -111,8 +252,55 @@ export class LegacyExternalInterface return typeof fn !== 'undefined' ? fn(...params) : undefined; } - public static addCallback(name: string, func: Function) + public static addCallback(name: string, func: Function): boolean { + // Validate callback name is not empty + if(!name || typeof name !== 'string' || name.trim().length === 0) + { + console.warn('[LegacyExternalInterface] Invalid callback name'); + return false; + } + + // Check against blocklist of dangerous global function names + // This prevents overwriting critical globals like 'eval', 'Function', etc. + if(this.BLOCKED_CALLBACK_NAMES.has(name)) + { + console.warn(`[LegacyExternalInterface] Blocked registration of dangerous callback: ${name}`); + return false; + } + + // Additional safety: prevent overwriting existing non-function properties + const existing = (window as any)[name]; + if(existing !== undefined && typeof existing !== 'function') + { + console.warn(`[LegacyExternalInterface] Cannot overwrite non-function property: ${name}`); + return false; + } + (window as any)[name] = func; + return true; + } + + public static removeCallback(name: string): boolean + { + // Validate callback name + if(!name || typeof name !== 'string' || name.trim().length === 0) + { + return false; + } + + // Don't allow removal of blocked/dangerous globals + if(this.BLOCKED_CALLBACK_NAMES.has(name)) + { + return false; + } + + if((window as any)[name] !== undefined) + { + delete (window as any)[name]; + return true; + } + + return false; } }