From 00a91da6eb82db8645195b1ede3f0311a7b2f3b2 Mon Sep 17 00:00:00 2001 From: Robby Date: Wed, 28 Jan 2026 10:51:16 +0000 Subject: [PATCH] security(gateway): validate transcript sessionFile path Problem: `resolveTranscriptPath()` returned user-provided `sessionFile` directly without any validation. An attacker could supply arbitrary paths like `/etc/cron.d/evil` to write outside the intended directory. Solution: - When storePath exists: validate sessionFile stays within store directory - When no storePath: require absolute path (reject relative paths without context) - Use path.relative() to detect escape attempts Fixes #3277 (partial) [AI-assisted: lightly tested, code understood] --- src/gateway/server-methods/chat.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 9010a6f21..2979e21cd 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -56,11 +56,24 @@ function resolveTranscriptPath(params: { sessionFile?: string; }): string | null { const { sessionId, storePath, sessionFile } = params; - if (sessionFile) return sessionFile; + if (sessionFile) { + if (storePath) { + const storeDir = path.dirname(storePath); + const absSessionFile = path.resolve(storeDir, sessionFile); + const rel = path.relative(storeDir, absSessionFile); + if (rel.startsWith("..") || path.isAbsolute(rel)) { + throw new Error("sessionFile escapes store directory"); + } + return absSessionFile; + } + if (!path.isAbsolute(sessionFile)) { + throw new Error("sessionFile must be absolute when storePath is not set"); + } + return sessionFile; + } if (!storePath) return null; return path.join(path.dirname(storePath), `${sessionId}.jsonl`); } - function ensureTranscriptFile(params: { transcriptPath: string; sessionId: string }): { ok: boolean; error?: string;