mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 13:44:58 +00:00
refactor(security): unify hardened install and fs write flows
This commit is contained in:
@@ -26,6 +26,11 @@ type PathSafetyOptions = {
|
||||
allowedType?: SafeOpenSyncAllowedType;
|
||||
};
|
||||
|
||||
type PathSafetyCheck = {
|
||||
target: SandboxResolvedFsPath;
|
||||
options: PathSafetyOptions;
|
||||
};
|
||||
|
||||
export type SandboxResolvedPath = {
|
||||
hostPath: string;
|
||||
relativePath: string;
|
||||
@@ -97,8 +102,9 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
signal?: AbortSignal;
|
||||
}): Promise<Buffer> {
|
||||
const target = this.resolveResolvedPath(params);
|
||||
await this.assertPathSafety(target, { action: "read files" });
|
||||
const result = await this.runCommand('set -eu; cat -- "$1"', {
|
||||
const result = await this.runCheckedCommand({
|
||||
checks: [{ target, options: { action: "read files" } }],
|
||||
script: 'set -eu; cat -- "$1"',
|
||||
args: [target.containerPath],
|
||||
signal: params.signal,
|
||||
});
|
||||
@@ -127,8 +133,10 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
});
|
||||
|
||||
try {
|
||||
await this.assertPathSafety(target, { action: "write files", requireWritable: true });
|
||||
await this.runCommand('set -eu; mv -f -- "$1" "$2"', {
|
||||
await this.runCheckedCommand({
|
||||
checks: [{ target, options: { action: "write files", requireWritable: true } }],
|
||||
recheckBeforeCommand: true,
|
||||
script: 'set -eu; mv -f -- "$1" "$2"',
|
||||
args: [tempPath, target.containerPath],
|
||||
signal: params.signal,
|
||||
});
|
||||
@@ -141,12 +149,18 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
async mkdirp(params: { filePath: string; cwd?: string; signal?: AbortSignal }): Promise<void> {
|
||||
const target = this.resolveResolvedPath(params);
|
||||
this.ensureWriteAccess(target, "create directories");
|
||||
await this.assertPathSafety(target, {
|
||||
action: "create directories",
|
||||
requireWritable: true,
|
||||
allowedType: "directory",
|
||||
});
|
||||
await this.runCommand('set -eu; mkdir -p -- "$1"', {
|
||||
await this.runCheckedCommand({
|
||||
checks: [
|
||||
{
|
||||
target,
|
||||
options: {
|
||||
action: "create directories",
|
||||
requireWritable: true,
|
||||
allowedType: "directory",
|
||||
},
|
||||
},
|
||||
],
|
||||
script: 'set -eu; mkdir -p -- "$1"',
|
||||
args: [target.containerPath],
|
||||
signal: params.signal,
|
||||
});
|
||||
@@ -161,21 +175,23 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
}): Promise<void> {
|
||||
const target = this.resolveResolvedPath(params);
|
||||
this.ensureWriteAccess(target, "remove files");
|
||||
await this.assertPathSafety(target, {
|
||||
action: "remove files",
|
||||
requireWritable: true,
|
||||
aliasPolicy: PATH_ALIAS_POLICIES.unlinkTarget,
|
||||
});
|
||||
const flags = [params.force === false ? "" : "-f", params.recursive ? "-r" : ""].filter(
|
||||
Boolean,
|
||||
);
|
||||
const rmCommand = flags.length > 0 ? `rm ${flags.join(" ")}` : "rm";
|
||||
await this.assertPathSafety(target, {
|
||||
action: "remove files",
|
||||
requireWritable: true,
|
||||
aliasPolicy: PATH_ALIAS_POLICIES.unlinkTarget,
|
||||
});
|
||||
await this.runCommand(`set -eu; ${rmCommand} -- "$1"`, {
|
||||
await this.runCheckedCommand({
|
||||
checks: [
|
||||
{
|
||||
target,
|
||||
options: {
|
||||
action: "remove files",
|
||||
requireWritable: true,
|
||||
aliasPolicy: PATH_ALIAS_POLICIES.unlinkTarget,
|
||||
},
|
||||
},
|
||||
],
|
||||
recheckBeforeCommand: true,
|
||||
script: `set -eu; ${rmCommand} -- "$1"`,
|
||||
args: [target.containerPath],
|
||||
signal: params.signal,
|
||||
});
|
||||
@@ -191,31 +207,30 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
const to = this.resolveResolvedPath({ filePath: params.to, cwd: params.cwd });
|
||||
this.ensureWriteAccess(from, "rename files");
|
||||
this.ensureWriteAccess(to, "rename files");
|
||||
await this.assertPathSafety(from, {
|
||||
action: "rename files",
|
||||
requireWritable: true,
|
||||
aliasPolicy: PATH_ALIAS_POLICIES.unlinkTarget,
|
||||
await this.runCheckedCommand({
|
||||
checks: [
|
||||
{
|
||||
target: from,
|
||||
options: {
|
||||
action: "rename files",
|
||||
requireWritable: true,
|
||||
aliasPolicy: PATH_ALIAS_POLICIES.unlinkTarget,
|
||||
},
|
||||
},
|
||||
{
|
||||
target: to,
|
||||
options: {
|
||||
action: "rename files",
|
||||
requireWritable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
recheckBeforeCommand: true,
|
||||
script:
|
||||
'set -eu; dir=$(dirname -- "$2"); if [ "$dir" != "." ]; then mkdir -p -- "$dir"; fi; mv -- "$1" "$2"',
|
||||
args: [from.containerPath, to.containerPath],
|
||||
signal: params.signal,
|
||||
});
|
||||
await this.assertPathSafety(to, {
|
||||
action: "rename files",
|
||||
requireWritable: true,
|
||||
});
|
||||
await this.assertPathSafety(from, {
|
||||
action: "rename files",
|
||||
requireWritable: true,
|
||||
aliasPolicy: PATH_ALIAS_POLICIES.unlinkTarget,
|
||||
});
|
||||
await this.assertPathSafety(to, {
|
||||
action: "rename files",
|
||||
requireWritable: true,
|
||||
});
|
||||
await this.runCommand(
|
||||
'set -eu; dir=$(dirname -- "$2"); if [ "$dir" != "." ]; then mkdir -p -- "$dir"; fi; mv -- "$1" "$2"',
|
||||
{
|
||||
args: [from.containerPath, to.containerPath],
|
||||
signal: params.signal,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async stat(params: {
|
||||
@@ -224,8 +239,9 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
signal?: AbortSignal;
|
||||
}): Promise<SandboxFsStat | null> {
|
||||
const target = this.resolveResolvedPath(params);
|
||||
await this.assertPathSafety(target, { action: "stat files" });
|
||||
const result = await this.runCommand('set -eu; stat -c "%F|%s|%Y" -- "$1"', {
|
||||
const result = await this.runCheckedCommand({
|
||||
checks: [{ target, options: { action: "stat files" } }],
|
||||
script: 'set -eu; stat -c "%F|%s|%Y" -- "$1"',
|
||||
args: [target.containerPath],
|
||||
signal: params.signal,
|
||||
allowFailure: true,
|
||||
@@ -272,6 +288,33 @@ class SandboxFsBridgeImpl implements SandboxFsBridge {
|
||||
});
|
||||
}
|
||||
|
||||
private async runCheckedCommand(params: {
|
||||
checks: PathSafetyCheck[];
|
||||
script: string;
|
||||
args?: string[];
|
||||
stdin?: Buffer | string;
|
||||
allowFailure?: boolean;
|
||||
signal?: AbortSignal;
|
||||
recheckBeforeCommand?: boolean;
|
||||
}): Promise<ExecDockerRawResult> {
|
||||
await this.assertPathChecks(params.checks);
|
||||
if (params.recheckBeforeCommand) {
|
||||
await this.assertPathChecks(params.checks);
|
||||
}
|
||||
return await this.runCommand(params.script, {
|
||||
args: params.args,
|
||||
stdin: params.stdin,
|
||||
allowFailure: params.allowFailure,
|
||||
signal: params.signal,
|
||||
});
|
||||
}
|
||||
|
||||
private async assertPathChecks(checks: PathSafetyCheck[]): Promise<void> {
|
||||
for (const check of checks) {
|
||||
await this.assertPathSafety(check.target, check.options);
|
||||
}
|
||||
}
|
||||
|
||||
private async assertPathSafety(target: SandboxResolvedFsPath, options: PathSafetyOptions) {
|
||||
const lexicalMount = this.resolveMountByContainerPath(target.containerPath);
|
||||
if (!lexicalMount) {
|
||||
|
||||
Reference in New Issue
Block a user