fix(worker): contain workspace paths against symlinks

This commit is contained in:
Gabriel Brown
2026-07-10 17:11:00 -04:00
parent 2bc090c378
commit 57c175aecd
4 changed files with 107 additions and 23 deletions
+10 -12
View File
@@ -34,6 +34,7 @@ import {
promptOpenCodeSession,
replyOpenCodePermission,
} from './opencode-session';
import { assertContainedRealPath } from './path-containment';
import { createRedactor, truncate } from './redact';
import {
listWorkspaceContainerNames,
@@ -1058,15 +1059,6 @@ const resolveWorkspace = (jobId: string) => {
return workspace;
};
const safeWorkspacePath = (repoDir: string, filePath: string) => {
const resolved = path.resolve(repoDir, filePath);
const root = path.resolve(repoDir);
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
throw new Error(`Refusing to access path outside repository: ${filePath}`);
}
return resolved;
};
const fileChangedType = async (repoDir: string, filePath: string) => {
const status = await run('git', ['status', '--short', '--', filePath], {
cwd: repoDir,
@@ -1169,7 +1161,11 @@ const recordChangedFiles = async (
const materializeEnvFile = async (workspace: ActiveWorkspace) => {
const { claim, repoDir } = workspace;
if (!claim.job.materializeEnvFile || !claim.job.envFilePath) return;
const envPath = safeWorkspacePath(repoDir, claim.job.envFilePath);
const envPath = await assertContainedRealPath(
repoDir,
claim.job.envFilePath,
{ forWrite: true },
);
await mkdir(path.dirname(envPath), { recursive: true });
const content = `${claim.secrets
.map((secret) => `${secret.name}=${JSON.stringify(secret.value)}`)
@@ -1503,7 +1499,7 @@ export const listWorkspaceTree = async (jobId: string) => {
export const readWorkspaceFile = async (jobId: string, filePath: string) => {
const workspace = resolveWorkspace(jobId);
const target = safeWorkspacePath(workspace.repoDir, filePath);
const target = await assertContainedRealPath(workspace.repoDir, filePath);
return await readFile(target, 'utf8');
};
@@ -1513,7 +1509,9 @@ export const writeWorkspaceFile = async (
content: string,
) => {
const workspace = resolveWorkspace(jobId);
const target = safeWorkspacePath(workspace.repoDir, filePath);
const target = await assertContainedRealPath(workspace.repoDir, filePath, {
forWrite: true,
});
await mkdir(path.dirname(target), { recursive: true });
await writeFile(target, content);
const diff = await getWorktreeDiff(workspace.repoDir, workspace.redact);