fix(security): harden structural session path fallback

This commit is contained in:
Peter Steinberger
2026-02-24 02:52:25 +00:00
parent ff4e6ca0d9
commit fefc414576
2 changed files with 79 additions and 5 deletions

View File

@@ -115,6 +115,39 @@ function extractAgentIdFromAbsoluteSessionPath(candidateAbsPath: string): string
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 {
try {
return fs.realpathSync(filePath);
@@ -170,11 +203,15 @@ function resolvePathWithinSessionsDir(
if (resolvedFromPath) {
return resolvedFromPath;
}
// The path structurally matches .../agents/<agentId>/sessions/...
// Accept it even if the root directory differs from the current env
// (e.g., OPENCLAW_STATE_DIR changed between session creation and resolution).
// The structural pattern provides sufficient containment guarantees.
return path.resolve(realTrimmed);
// Cross-root compatibility for older absolute paths:
// keep only canonical .../agents/<agentId>/sessions/<file> shapes.
const structuralFallback = resolveStructuralSessionFallbackPath(
realTrimmed,
extractedAgentId,
);
if (structuralFallback) {
return structuralFallback;
}
}
}
if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) {