fix: preserve sessions path containment after legacy absolute-path normalization (#15323) (thanks @mudrii)

This commit is contained in:
Peter Steinberger
2026-02-13 15:13:39 +01:00
parent abcdbd8afc
commit 0d0effd9d4
3 changed files with 15 additions and 5 deletions

View File

@@ -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. - 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` 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/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. - 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 ## 2026.2.12

View File

@@ -55,6 +55,14 @@ describe("session path safety", () => {
resolveSessionFilePath("sess-1", { sessionFile: "../../etc/passwd" }, { sessionsDir }), resolveSessionFilePath("sess-1", { sessionFile: "../../etc/passwd" }, { sessionsDir }),
).toThrow(/within sessions directory/); ).toThrow(/within sessions directory/);
expect(() =>
resolveSessionFilePath(
"sess-1",
{ sessionFile: "subdir/../../escape.jsonl" },
{ sessionsDir },
),
).toThrow(/within sessions directory/);
expect(() => expect(() =>
resolveSessionFilePath("sess-1", { sessionFile: "/etc/passwd" }, { sessionsDir }), resolveSessionFilePath("sess-1", { sessionFile: "/etc/passwd" }, { sessionsDir }),
).toThrow(/within sessions directory/); ).toThrow(/within sessions directory/);

View File

@@ -77,14 +77,15 @@ function resolvePathWithinSessionsDir(sessionsDir: string, candidate: string): s
throw new Error("Session file path must not be empty"); throw new Error("Session file path must not be empty");
} }
const resolvedBase = path.resolve(sessionsDir); const resolvedBase = path.resolve(sessionsDir);
// Normalize absolute paths that are within the sessions directory. // Older versions stored absolute sessionFile paths in sessions.json.
// Older versions stored absolute sessionFile paths in sessions.json; // Preserve compatibility, but validate containment against the resolved path.
// convert them to relative so the containment check passes.
const normalized = path.isAbsolute(trimmed) ? path.relative(resolvedBase, trimmed) : trimmed; 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"); throw new Error("Session file path must be within sessions directory");
} }
return path.resolve(resolvedBase, normalized); return resolvedCandidate;
} }
export function resolveSessionTranscriptPathInDir( export function resolveSessionTranscriptPathInDir(