mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-29 09:41:08 +00:00
fix(security): harden structural session path fallback
This commit is contained in:
@@ -561,6 +561,43 @@ describe("sessions", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back when structural cross-root path traverses after sessions", () => {
|
||||||
|
withStateDir(path.resolve("/different/state"), () => {
|
||||||
|
const originalBase = path.resolve("/original/state");
|
||||||
|
const unsafe = path.join(originalBase, "agents", "bot2", "sessions", "..", "..", "etc");
|
||||||
|
const sessionFile = resolveSessionFilePath(
|
||||||
|
"sess-1",
|
||||||
|
{ sessionFile: path.join(unsafe, "passwd") },
|
||||||
|
{ agentId: "bot1" },
|
||||||
|
);
|
||||||
|
expect(sessionFile).toBe(
|
||||||
|
path.join(path.resolve("/different/state"), "agents", "bot1", "sessions", "sess-1.jsonl"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back when structural cross-root path nests under sessions", () => {
|
||||||
|
withStateDir(path.resolve("/different/state"), () => {
|
||||||
|
const originalBase = path.resolve("/original/state");
|
||||||
|
const nested = path.join(
|
||||||
|
originalBase,
|
||||||
|
"agents",
|
||||||
|
"bot2",
|
||||||
|
"sessions",
|
||||||
|
"nested",
|
||||||
|
"sess-1.jsonl",
|
||||||
|
);
|
||||||
|
const sessionFile = resolveSessionFilePath(
|
||||||
|
"sess-1",
|
||||||
|
{ sessionFile: nested },
|
||||||
|
{ agentId: "bot1" },
|
||||||
|
);
|
||||||
|
expect(sessionFile).toBe(
|
||||||
|
path.join(path.resolve("/different/state"), "agents", "bot1", "sessions", "sess-1.jsonl"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("falls back to derived transcript path when sessionFile is outside agent sessions directories", () => {
|
it("falls back to derived transcript path when sessionFile is outside agent sessions directories", () => {
|
||||||
withStateDir(path.resolve("/home/user/.openclaw"), () => {
|
withStateDir(path.resolve("/home/user/.openclaw"), () => {
|
||||||
const sessionFile = resolveSessionFilePath(
|
const sessionFile = resolveSessionFilePath(
|
||||||
|
|||||||
@@ -115,6 +115,39 @@ function extractAgentIdFromAbsoluteSessionPath(candidateAbsPath: string): string
|
|||||||
return agentId || undefined;
|
return agentId || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveStructuralSessionFallbackPath(
|
||||||
|
candidateAbsPath: string,
|
||||||
|
expectedAgentId: string,
|
||||||
|
): string | undefined {
|
||||||
|
const normalized = path.normalize(path.resolve(candidateAbsPath));
|
||||||
|
const parts = normalized.split(path.sep).filter(Boolean);
|
||||||
|
const sessionsIndex = parts.lastIndexOf("sessions");
|
||||||
|
if (sessionsIndex < 2 || parts[sessionsIndex - 2] !== "agents") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const agentIdPart = parts[sessionsIndex - 1];
|
||||||
|
if (!agentIdPart) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const normalizedAgentId = normalizeAgentId(agentIdPart);
|
||||||
|
if (normalizedAgentId !== agentIdPart.toLowerCase()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (normalizedAgentId !== normalizeAgentId(expectedAgentId)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const relativeSegments = parts.slice(sessionsIndex + 1);
|
||||||
|
// Session transcripts are stored as direct files in "sessions/".
|
||||||
|
if (relativeSegments.length !== 1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const fileName = relativeSegments[0];
|
||||||
|
if (!fileName || fileName === "." || fileName === "..") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
function safeRealpathSync(filePath: string): string | undefined {
|
function safeRealpathSync(filePath: string): string | undefined {
|
||||||
try {
|
try {
|
||||||
return fs.realpathSync(filePath);
|
return fs.realpathSync(filePath);
|
||||||
@@ -170,11 +203,15 @@ function resolvePathWithinSessionsDir(
|
|||||||
if (resolvedFromPath) {
|
if (resolvedFromPath) {
|
||||||
return resolvedFromPath;
|
return resolvedFromPath;
|
||||||
}
|
}
|
||||||
// The path structurally matches .../agents/<agentId>/sessions/...
|
// Cross-root compatibility for older absolute paths:
|
||||||
// Accept it even if the root directory differs from the current env
|
// keep only canonical .../agents/<agentId>/sessions/<file> shapes.
|
||||||
// (e.g., OPENCLAW_STATE_DIR changed between session creation and resolution).
|
const structuralFallback = resolveStructuralSessionFallbackPath(
|
||||||
// The structural pattern provides sufficient containment guarantees.
|
realTrimmed,
|
||||||
return path.resolve(realTrimmed);
|
extractedAgentId,
|
||||||
|
);
|
||||||
|
if (structuralFallback) {
|
||||||
|
return structuralFallback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) {
|
if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user