Files
Nitro_Render_V3/packages/utils/src/LegacyExternalInterface.ts
T
DuckieTM 4f2c2f904c 🆙 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.
2026-01-31 16:18:03 +01:00

307 lines
9.7 KiB
TypeScript

declare global
{
interface Window
{
FlashExternalInterface?:
{
legacyTrack?: (
category: string,
action: string,
data: unknown[]
) => void;
logDebug?: (...params: string[]) => void;
disconnect?: (reasonCode: number, reasonString: string) => void;
logout?: () => void;
openWebPageAndMinimizeClient?: (pageUrl: string) => void;
heartBeat?: () => void;
logEventLog?: (log: string) => void;
openPage?: (pageUrl: string) => void;
closeWebPageAndRestoreClient?: () => void;
openHabblet?: (name: string, param: string) => void;
closeHabblet?: (name: string, param: string) => void;
openExternalLink?: (link: string) => void;
roomVisited?: (roomId: number) => void;
openMinimail?: (target: string) => void;
openNews?: () => void;
closeNews?: () => void;
openAvatars?: () => void;
openRoomEnterAd?: () => void;
updateFigure?: (figure: string) => void;
};
FlashExternalGameInterface?:
{
showGame?: (url: string) => void;
hideGame?: () => void;
};
}
}
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;
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))
{
try
{
const { method, params } = JSON.parse(
ev.data.substring(LegacyExternalInterface.MESSAGE_KEY.length)
);
// Validate method is in whitelist before executing
if(!this.ALLOWED_EXTERNAL_METHODS.has(method))
{
console.warn(`[LegacyExternalInterface] Blocked unauthorized method call: ${method}`);
return;
}
// 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]>
): ReturnType<typeof window.FlashExternalInterface[K]> | undefined
{
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;
const fn = window.FlashExternalInterface[method] as Function;
return typeof fn !== 'undefined' ? fn(...params) : undefined;
}
public static callGame<K extends keyof typeof window.FlashExternalGameInterface>(
method: K,
...params: Parameters<typeof window.FlashExternalGameInterface[K]>
): ReturnType<typeof window.FlashExternalGameInterface[K]> | undefined
{
if(window.top !== window)
{
// 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;
const fn = window.FlashExternalGameInterface[method] as Function;
return typeof fn !== 'undefined' ? fn(...params) : undefined;
}
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;
}
}