refactor(security): unify hardened install and fs write flows

This commit is contained in:
Peter Steinberger
2026-03-02 17:23:24 +00:00
parent d3e8b17aa6
commit d4bf07d075
9 changed files with 435 additions and 441 deletions

View File

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