mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 08:50:21 +00:00
fix: harden workspace boundary path resolution
This commit is contained in:
@@ -30,7 +30,8 @@ import { loadConfig, writeConfigFile } from "../../config/config.js";
|
||||
import { resolveSessionTranscriptsDirForAgent } from "../../config/sessions/paths.js";
|
||||
import { sameFileIdentity } from "../../infra/file-identity.js";
|
||||
import { SafeOpenError, readLocalFileSafely } from "../../infra/fs-safe.js";
|
||||
import { isNotFoundPathError, isPathInside } from "../../infra/path-guards.js";
|
||||
import { assertNoPathAliasEscape } from "../../infra/path-alias-guards.js";
|
||||
import { isNotFoundPathError } from "../../infra/path-guards.js";
|
||||
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../routing/session-key.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import {
|
||||
@@ -143,8 +144,19 @@ async function resolveAgentWorkspaceFilePath(params: {
|
||||
const requestPath = path.join(params.workspaceDir, params.name);
|
||||
const workspaceReal = await resolveWorkspaceRealPath(params.workspaceDir);
|
||||
const candidatePath = path.resolve(workspaceReal, params.name);
|
||||
if (!isPathInside(workspaceReal, candidatePath)) {
|
||||
return { kind: "invalid", requestPath, reason: "path escapes workspace root" };
|
||||
|
||||
try {
|
||||
await assertNoPathAliasEscape({
|
||||
absolutePath: candidatePath,
|
||||
rootPath: workspaceReal,
|
||||
boundaryLabel: "workspace root",
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
kind: "invalid",
|
||||
requestPath,
|
||||
reason: error instanceof Error ? error.message : "path escapes workspace root",
|
||||
};
|
||||
}
|
||||
|
||||
let candidateLstat: Awaited<ReturnType<typeof fs.lstat>>;
|
||||
@@ -169,27 +181,28 @@ async function resolveAgentWorkspaceFilePath(params: {
|
||||
if (params.allowMissing) {
|
||||
return { kind: "missing", requestPath, ioPath: candidatePath, workspaceReal };
|
||||
}
|
||||
return { kind: "invalid", requestPath, reason: "symlink target not found" };
|
||||
return { kind: "invalid", requestPath, reason: "file not found" };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (!isPathInside(workspaceReal, targetReal)) {
|
||||
return { kind: "invalid", requestPath, reason: "symlink target escapes workspace root" };
|
||||
}
|
||||
let targetStat: Awaited<ReturnType<typeof fs.stat>>;
|
||||
try {
|
||||
const targetStat = await fs.stat(targetReal);
|
||||
if (!targetStat.isFile()) {
|
||||
return { kind: "invalid", requestPath, reason: "symlink target is not a file" };
|
||||
}
|
||||
if (targetStat.nlink > 1) {
|
||||
return { kind: "invalid", requestPath, reason: "hardlinked file target not allowed" };
|
||||
}
|
||||
targetStat = await fs.stat(targetReal);
|
||||
} catch (err) {
|
||||
if (isNotFoundPathError(err) && params.allowMissing) {
|
||||
return { kind: "missing", requestPath, ioPath: targetReal, workspaceReal };
|
||||
if (isNotFoundPathError(err)) {
|
||||
if (params.allowMissing) {
|
||||
return { kind: "missing", requestPath, ioPath: targetReal, workspaceReal };
|
||||
}
|
||||
return { kind: "invalid", requestPath, reason: "file not found" };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (!targetStat.isFile()) {
|
||||
return { kind: "invalid", requestPath, reason: "path is not a regular file" };
|
||||
}
|
||||
if (targetStat.nlink > 1) {
|
||||
return { kind: "invalid", requestPath, reason: "hardlinked file path not allowed" };
|
||||
}
|
||||
return { kind: "ready", requestPath, ioPath: targetReal, workspaceReal };
|
||||
}
|
||||
|
||||
@@ -200,11 +213,8 @@ async function resolveAgentWorkspaceFilePath(params: {
|
||||
return { kind: "invalid", requestPath, reason: "hardlinked file path not allowed" };
|
||||
}
|
||||
|
||||
const candidateReal = await fs.realpath(candidatePath).catch(() => candidatePath);
|
||||
if (!isPathInside(workspaceReal, candidateReal)) {
|
||||
return { kind: "invalid", requestPath, reason: "resolved file escapes workspace root" };
|
||||
}
|
||||
return { kind: "ready", requestPath, ioPath: candidateReal, workspaceReal };
|
||||
const targetReal = await fs.realpath(candidatePath).catch(() => candidatePath);
|
||||
return { kind: "ready", requestPath, ioPath: targetReal, workspaceReal };
|
||||
}
|
||||
|
||||
async function statFileSafely(filePath: string): Promise<FileMeta | null> {
|
||||
|
||||
Reference in New Issue
Block a user