From e83d70116f20f0b77a3436cdca7ac15b8e7cebe8 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 30 Aug 2026 13:48:34 -0400 Subject: [PATCH] Room ids pass a strict alphabet before touching the filesystem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lazy room wake handed caller-supplied ids to fileFor; an 8-char traversal like '../../aa' fit through the length trim. Only [A-Z0-9] codes reach disk now — reads refuse quietly, server-minted writes fail loud. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF --- packages/server/src/store.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 {