Room ids pass a strict alphabet before touching the filesystem

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-30 13:48:34 -04:00
co-authored by Claude Fable 5
parent 002698085f
commit e83d70116f
+9 -1
View File
@@ -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 {