From abcdbd8afc416af336f211cac653b69de477a627 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 3ca4cdb9b2..baa45079bf 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 f123390a55..a630f68c2f 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(