From 25950bcbb8ba4d8cde002557f6e27c219ae4deda Mon Sep 17 00:00:00 2001 From: Ion Mudreac Date: Fri, 13 Feb 2026 16:51:46 +0800 Subject: [PATCH] fix(sessions): normalize absolute sessionFile paths for v2026.2.12 compatibility Older OpenClaw versions stored absolute sessionFile paths in sessions.json. v2026.2.12 added path traversal security that rejected these absolute paths, breaking all Telegram group handlers with 'Session file path must be within sessions directory' errors. Changes: - resolvePathWithinSessionsDir() now normalizes absolute paths that resolve within the sessions directory, converting them to relative before validation - Added 3 tests for absolute path handling (within dir, with topic, outside dir) Fixes #15283 Closes #15214, #15237, #15216, #15152, #15213 --- src/config/sessions/paths.test.ts | 36 +++++++++++++++++++++++++++++++ src/config/sessions/paths.ts | 10 +++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/config/sessions/paths.test.ts b/src/config/sessions/paths.test.ts index 3ca4cdb9b..baa45079b 100644 --- a/src/config/sessions/paths.test.ts +++ b/src/config/sessions/paths.test.ts @@ -72,6 +72,42 @@ describe("session path safety", () => { expect(resolved).toBe(path.resolve(sessionsDir, "subdir/threaded-session.jsonl")); }); + it("accepts absolute sessionFile paths that resolve within the sessions dir", () => { + const sessionsDir = "/tmp/openclaw/agents/main/sessions"; + + const resolved = resolveSessionFilePath( + "sess-1", + { sessionFile: "/tmp/openclaw/agents/main/sessions/abc-123.jsonl" }, + { sessionsDir }, + ); + + expect(resolved).toBe(path.resolve(sessionsDir, "abc-123.jsonl")); + }); + + it("accepts absolute sessionFile with topic suffix within the sessions dir", () => { + const sessionsDir = "/tmp/openclaw/agents/main/sessions"; + + const resolved = resolveSessionFilePath( + "sess-1", + { sessionFile: "/tmp/openclaw/agents/main/sessions/abc-123-topic-42.jsonl" }, + { sessionsDir }, + ); + + expect(resolved).toBe(path.resolve(sessionsDir, "abc-123-topic-42.jsonl")); + }); + + it("rejects absolute sessionFile paths outside the sessions dir", () => { + const sessionsDir = "/tmp/openclaw/agents/main/sessions"; + + expect(() => + resolveSessionFilePath( + "sess-1", + { sessionFile: "/tmp/openclaw/agents/work/sessions/abc-123.jsonl" }, + { sessionsDir }, + ), + ).toThrow(/within sessions directory/); + }); + it("uses agent sessions dir fallback for transcript path", () => { const resolved = resolveSessionTranscriptPath("sess-1", "main"); expect(resolved.endsWith(path.join("agents", "main", "sessions", "sess-1.jsonl"))).toBe(true); diff --git a/src/config/sessions/paths.ts b/src/config/sessions/paths.ts index f123390a5..a630f68c2 100644 --- a/src/config/sessions/paths.ts +++ b/src/config/sessions/paths.ts @@ -77,12 +77,14 @@ function resolvePathWithinSessionsDir(sessionsDir: string, candidate: string): s throw new Error("Session file path must not be empty"); } const resolvedBase = path.resolve(sessionsDir); - const resolvedCandidate = path.resolve(resolvedBase, trimmed); - const relative = path.relative(resolvedBase, resolvedCandidate); - if (relative.startsWith("..") || path.isAbsolute(relative)) { + // Normalize absolute paths that are within the sessions directory. + // Older versions stored absolute sessionFile paths in sessions.json; + // convert them to relative so the containment check passes. + const normalized = path.isAbsolute(trimmed) ? path.relative(resolvedBase, trimmed) : trimmed; + if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) { throw new Error("Session file path must be within sessions directory"); } - return resolvedCandidate; + return path.resolve(resolvedBase, normalized); } export function resolveSessionTranscriptPathInDir(