diff --git a/packages/server/src/store.ts b/packages/server/src/store.ts index aec9938..d0ca44f 100644 --- a/packages/server/src/store.ts +++ b/packages/server/src/store.ts @@ -75,7 +75,14 @@ export type RoomLine = RoomMetaLine | JoinLine | StartLine | CommandLine | ChatL const DATA_DIR = process.env.WIZWAR_DATA_DIR ?? join(process.cwd(), "data", "rooms"); +/** Room ids reach the filesystem, so only the room-code alphabet may pass — + * anything else (dots, slashes, control bytes) is a traversal attempt. */ +function safeRoomId(roomId: string): boolean { + return /^[A-Z0-9]{1,8}$/.test(roomId); +} + function fileFor(roomId: string): string { + if (!safeRoomId(roomId)) throw new Error(`unsafe room id: ${JSON.stringify(roomId)}`); return join(DATA_DIR, `${roomId}.jsonl`); } @@ -90,7 +97,7 @@ export function ensureDataDir(): void { /** A room file may exist even when the room failed to restore into memory. */ export function roomFileExists(roomId: string): boolean { - return existsSync(fileFor(roomId)); + return safeRoomId(roomId) && existsSync(fileFor(roomId)); } export function appendLine(roomId: string, line: RoomLine): void { @@ -99,6 +106,7 @@ export function appendLine(roomId: string, line: RoomLine): void { /** Read one persisted room's lines, or null if it has no ledger. */ export function readRoom(roomId: string): RoomLine[] | null { + if (!safeRoomId(roomId)) return null; const file = fileFor(roomId); if (!existsSync(file)) return null; try {