diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd76c619b..9e6e7b3633 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai - Outbound/Threading: pass `replyTo` and `threadId` from `message send` tool actions through the core outbound send path to channel adapters, preserving thread/reply routing. (#14948) Thanks @mcaxtr. - Sessions/Agents: pass `agentId` when resolving existing transcript paths in reply runs so non-default agents and heartbeat/chat handlers no longer fail with `Session file path must be within sessions directory`. (#15141) Thanks @Goldenmonstew. - Sessions/Agents: pass `agentId` through status and usage transcript-resolution paths (auto-reply, gateway usage APIs, and session cost/log loaders) so non-default agents can resolve absolute session files without path-validation failures. (#15103) Thanks @jalehman. +- Sessions: accept legacy absolute `sessionFile` paths from prior releases while preserving containment checks to block traversal escapes. (#15323) Thanks @mudrii. - Signal/Install: auto-install `signal-cli` via Homebrew on non-x64 Linux architectures, avoiding x86_64 native binary `Exec format error` failures on arm64/arm hosts. (#15443) Thanks @jogvan-k. ## 2026.2.12 diff --git a/src/config/sessions/paths.test.ts b/src/config/sessions/paths.test.ts index baa45079bf..92191b8a45 100644 --- a/src/config/sessions/paths.test.ts +++ b/src/config/sessions/paths.test.ts @@ -55,6 +55,14 @@ describe("session path safety", () => { resolveSessionFilePath("sess-1", { sessionFile: "../../etc/passwd" }, { sessionsDir }), ).toThrow(/within sessions directory/); + expect(() => + resolveSessionFilePath( + "sess-1", + { sessionFile: "subdir/../../escape.jsonl" }, + { sessionsDir }, + ), + ).toThrow(/within sessions directory/); + expect(() => resolveSessionFilePath("sess-1", { sessionFile: "/etc/passwd" }, { sessionsDir }), ).toThrow(/within sessions directory/); diff --git a/src/config/sessions/paths.ts b/src/config/sessions/paths.ts index a630f68c2f..407c6cd420 100644 --- a/src/config/sessions/paths.ts +++ b/src/config/sessions/paths.ts @@ -77,14 +77,15 @@ function resolvePathWithinSessionsDir(sessionsDir: string, candidate: string): s throw new Error("Session file path must not be empty"); } const resolvedBase = path.resolve(sessionsDir); - // 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. + // Older versions stored absolute sessionFile paths in sessions.json. + // Preserve compatibility, but validate containment against the resolved path. const normalized = path.isAbsolute(trimmed) ? path.relative(resolvedBase, trimmed) : trimmed; - if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) { + const resolvedCandidate = path.resolve(resolvedBase, normalized); + const relative = path.relative(resolvedBase, resolvedCandidate); + if (!normalized || relative.startsWith("..") || path.isAbsolute(relative)) { throw new Error("Session file path must be within sessions directory"); } - return path.resolve(resolvedBase, normalized); + return resolvedCandidate; } export function resolveSessionTranscriptPathInDir(