diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 85f00f8c0..728eeae2e 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -336,10 +336,20 @@ export class GameServer { return; } - const clientMsg = decodeBinaryClientGameplayMessage( - rawDataToUint8Array(message), - this.binaryContext, - ); + let clientMsg; + try { + clientMsg = decodeBinaryClientGameplayMessage( + rawDataToUint8Array(message), + this.binaryContext, + ); + } catch (e) { + this.log.warn(`Failed to parse client binary message, kicking`, { + clientID: client.clientID, + error: String(e), + }); + this.kickClient(client.clientID, KICK_REASON_INVALID_MESSAGE); + return; + } if (!this.checkRateLimit(client, clientMsg.type, bytes)) { return; } diff --git a/tests/server/GameServerBinaryProtocol.test.ts b/tests/server/GameServerBinaryProtocol.test.ts index cc05bfda8..4f34b6e1b 100644 --- a/tests/server/GameServerBinaryProtocol.test.ts +++ b/tests/server/GameServerBinaryProtocol.test.ts @@ -237,6 +237,38 @@ describe("GameServer binary gameplay protocol", () => { ); }); + it("kicks malformed binary gameplay messages after start", () => { + const logger = createMockLogger(); + const game = new GameServer( + "TEST0004", + logger as any, + Date.now(), + { + turnIntervalMs: () => 100, + env: () => 0, + } as any, + createGameConfig(), + ); + + const clientA = createClient( + "P0000001", + "55555555-5555-4555-8555-555555555555", + "Alice", + ); + + expect(game.joinClient(clientA.client)).toBe("joined"); + game.start(); + + clientA.ws.sent.length = 0; + clientA.ws.emit("message", new Uint8Array([99, 1, 0, 0]), true); + + expect(clientA.ws.sent).toEqual([ + expect.stringContaining('"error":"kick_reason.invalid_message"'), + ]); + expect(clientA.ws.readyState).toBe(3); + expect(game.activeClients).toHaveLength(0); + }); + it("accepts JSON rejoin after start and responds with JSON start", () => { const logger = createMockLogger(); const game = new GameServer(