diff --git a/src/server/InputHandler.ts b/src/server/InputHandler.ts index 77ffe46..3d79611 100644 --- a/src/server/InputHandler.ts +++ b/src/server/InputHandler.ts @@ -14,19 +14,38 @@ export interface InputMessage { } export class InputHandler { + // Security Constants (ADDED) + private static readonly MAX_TEXT_LENGTH = 500; + private static readonly MAX_DELTA = 2000; + private static readonly MAX_SCROLL = 500; + private static readonly MAX_ZOOM_INPUT = 1000; + private static readonly MAX_COMBO_KEYS = 5; + constructor() { mouse.config.mouseSpeed = 1000; } + // Utility clamp (ADDED) + private clamp(value: number, min: number, max: number): number { + if (!Number.isFinite(value)) return 0; + return Math.max(min, Math.min(max, value)); + } + async handleMessage(msg: InputMessage) { switch (msg.type) { + case 'move': if (msg.dx !== undefined && msg.dy !== undefined) { + + // Clamp dx/dy + const safeDx = this.clamp(msg.dx, -InputHandler.MAX_DELTA, InputHandler.MAX_DELTA); + const safeDy = this.clamp(msg.dy, -InputHandler.MAX_DELTA, InputHandler.MAX_DELTA); + const currentPos = await mouse.getPosition(); await mouse.setPosition(new Point( - currentPos.x + msg.dx, - currentPos.y + msg.dy + currentPos.x + safeDx, + currentPos.y + safeDy )); } break; @@ -45,21 +64,30 @@ export class InputHandler { case 'scroll': const promises: Promise[] = []; + //Clamp scroll values + const safeScrollY = typeof msg.dy === 'number' + ? this.clamp(msg.dy, -InputHandler.MAX_SCROLL, InputHandler.MAX_SCROLL) + : 0; + + const safeScrollX = typeof msg.dx === 'number' + ? this.clamp(msg.dx, -InputHandler.MAX_SCROLL, InputHandler.MAX_SCROLL) + : 0; + // Vertical scroll - if (typeof msg.dy === 'number' && msg.dy !== 0) { - if (msg.dy > 0) { - promises.push(mouse.scrollDown(msg.dy)); + if (safeScrollY !== 0) { + if (safeScrollY > 0) { + promises.push(mouse.scrollDown(safeScrollY)); } else { - promises.push(mouse.scrollUp(-msg.dy)); + promises.push(mouse.scrollUp(-safeScrollY)); } } // Horizontal scroll - if (typeof msg.dx === 'number' && msg.dx !== 0) { - if (msg.dx > 0) { - promises.push(mouse.scrollRight(msg.dx)); + if (safeScrollX !== 0) { + if (safeScrollX > 0) { + promises.push(mouse.scrollRight(safeScrollX)); } else { - promises.push(mouse.scrollLeft(-msg.dx)); + promises.push(mouse.scrollLeft(-safeScrollX)); } } @@ -70,12 +98,20 @@ export class InputHandler { case 'zoom': if (msg.delta !== undefined && msg.delta !== 0) { + + // Clamp incoming zoom delta + const safeDelta = this.clamp( + msg.delta, + -InputHandler.MAX_ZOOM_INPUT, + InputHandler.MAX_ZOOM_INPUT + ); + const sensitivityFactor = 0.5; const MAX_ZOOM_STEP = 5; const scaledDelta = - Math.sign(msg.delta) * - Math.min(Math.abs(msg.delta) * sensitivityFactor, MAX_ZOOM_STEP); + Math.sign(safeDelta) * + Math.min(Math.abs(safeDelta) * sensitivityFactor, MAX_ZOOM_STEP); const amount = -scaledDelta; @@ -91,23 +127,56 @@ export class InputHandler { case 'key': if (msg.key) { console.log(`Processing key: ${msg.key}`); - const nutKey = KEY_MAP[msg.key.toLowerCase()]; + const lowerKey = msg.key.toLowerCase(); + + // Allowlist validation + if (!(lowerKey in KEY_MAP) && lowerKey.length !== 1) { + console.warn(`Blocked unknown key: ${msg.key}`); + return; + } + + const nutKey = KEY_MAP[lowerKey]; + if (nutKey !== undefined) { await keyboard.type(nutKey); - } else if (msg.key.length === 1) { - await keyboard.type(msg.key); - } else { - console.log(`Unmapped key: ${msg.key}`); + } else if (lowerKey.length === 1) { + await keyboard.type(lowerKey); } } break; case 'combo': if (msg.keys && msg.keys.length > 0) { - const nutKeys: (Key | string)[] = []; + + // Limit combo length + if (msg.keys.length > InputHandler.MAX_COMBO_KEYS) { + console.warn('Combo too long — blocked'); + return; + } + + // Remove duplicate keys + const seen = new Set(); + const uniqueKeys: string[] = []; for (const k of msg.keys) { + const lower = k.toLowerCase(); + if (!seen.has(lower)) { + seen.add(lower); + uniqueKeys.push(k); // preserve original for single-char typing + } + } + + const nutKeys: (Key | string)[] = []; + + for (const k of uniqueKeys) { const lowerKey = k.toLowerCase(); + + if (!(lowerKey in KEY_MAP) && lowerKey.length !== 1) { + console.warn(`Blocked unknown key in combo: ${k}`); + continue; + } + const nutKey = KEY_MAP[lowerKey]; + if (nutKey !== undefined) { nutKeys.push(nutKey); } else if (lowerKey.length === 1) { @@ -148,6 +217,13 @@ export class InputHandler { case 'text': if (msg.text) { + + // Limit text length + if (msg.text.length > InputHandler.MAX_TEXT_LENGTH) { + console.warn('Text input too long — blocked'); + return; + } + await keyboard.type(msg.text); } break;