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
This commit is contained in:
Ion Mudreac
2026-02-13 16:51:46 +08:00
committed by Peter Steinberger
parent edfdd12d37
commit abcdbd8afc
2 changed files with 42 additions and 4 deletions

View File

@@ -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);

View File

@@ -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(