🆙 Updates

* 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.
This commit is contained in:
DuckieTM
2026-01-31 16:18:03 +01:00
parent eb4fe80612
commit 4f2c2f904c
2 changed files with 202 additions and 14 deletions
@@ -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);
+200 -12
View File
@@ -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<string> = 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<string> = 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<string> = 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<K extends keyof typeof window.FlashExternalInterface>(
method: K,
...params: Parameters<typeof window.FlashExternalInterface[K]>
@@ -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;
}
}