Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Steinberger
0d0effd9d4 fix: preserve sessions path containment after legacy absolute-path normalization (#15323) (thanks @mudrii) 2026-02-13 15:13:39 +01:00
Ion Mudreac
abcdbd8afc 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
2026-02-13 15:09:13 +01:00
3 changed files with 50 additions and 2 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.
- 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

View File

@@ -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/);
@@ -72,6 +80,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);

View File

@@ -77,9 +77,12 @@ 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);
// 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;
const resolvedCandidate = path.resolve(resolvedBase, normalized);
const relative = path.relative(resolvedBase, resolvedCandidate);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
if (!normalized || relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error("Session file path must be within sessions directory");
}
return resolvedCandidate;