# OpenShell Vulnerability Summary ## Vulnerability Overview OpenShell is a tool that allows arbitrary code execution within a sandboxed environment. This vulnerability involves the bypassing of sandbox boundaries, enabling malicious code to execute outside the sandbox. Specifically, the issue manifested as follows: - Sandboxing synchronization boundaries were hardened to prevent malicious code from escaping. - Sandboxing tests were optimized to ensure sandbox integrity. - Trusted image symbolic links were preserved to prevent malicious symlink attacks. - Global image file system boundaries were bound to prevent cross-file system attacks. ## Impact Scope - **Affected Versions**: v2026.4.26 to v2026.3.31-beta.1 - **Affected Component**: OpenShell's sandboxing mechanism - **Potential Risk**: Malicious code may execute outside the sandbox, leading to complete system compromise. ## Remediation - **Code Changes**: - In `extensions/openshell/src/hackend.ts`, hardening measures for sandbox synchronization boundaries were added to prevent malicious code escape. - In `extensions/openshell/src/mirror.ts`, sandboxing tests were optimized to ensure sandbox integrity. - In `extensions/openshell/src/mirror.ts`, trusted image symbolic links were preserved to prevent malicious symlink attacks. - In `extensions/openshell/src/mirror.ts`, global image file system boundaries were bound to prevent cross-file system attacks. ### Key Code Changes ```typescript // extensions/openshell/src/hackend.ts import { replaceDirectoryContents } from "./mirror.js"; const DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS = [ "hooks", "git-hooks", ".git", ]; function createExcludeMatcher(excludeDirs?: readonly string[]) { const excluded = new Set(excludeDirs ?? DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS); return (name: string) => excluded.has(name.toLowerCase()); } function createConcurrencyLimiter(limit: number) { let active = 0; const queue: Array void> = []; const release = () => { active--; queue.shift()?.(); }; return async (task: () => Promise): Promise => { if (active >= limit) { await new Promise((resolve) => { queue.push(resolve); }); } active++; try { return await task(); } finally { release(); } }; } const runLimitedFs = createConcurrencyLimiter(COPY_TREE_FS_CONCURRENCY); async function listAllFiles(targetPath: string) { return await runLimitedFs(async () => await fs.lstat(targetPath).catch(() => null)); } async function copyTreeWithoutSymlinks(params: { sourcePath: string; targetPath: string; preserveTargetSymlinks: boolean; }): Promise { const stats = await runLimitedFs(async () => await fs.lstat(params.sourcePath)); // Mirror sync only carries regular files and directories across the // host/sandbox boundary. Symlinks and special files are dropped. if (stats.isSymbolicLink()) { return; } const targetStats = await listAllFiles(params.targetPath); if (params.preserveTargetSymlinks && targetStats?.isSymbolicLink()) { return; } if (stats.isDirectory()) { await fs.mkdir(params.targetPath, { recursive: true }); const entries = await runLimitedFs(async () => await fs.readdir(params.sourcePath)); await Promise.all( entries.map(async (entry) => { await copyTreeWithoutSymlinks({ sourcePath: path.join(params.sourcePath, entry), targetPath: path.join(params.targetPath, entry), preserveTargetSymlinks: params.preserveTargetSymlinks, }); }) ); } else if (stats.isFile()) { await fs.copyFile(params.sourcePath, params.targetPath); } } async function stageDirectoryContents({ sourceDir, targetDir, }: { sourceDir: string; targetDir: string; }): Promise { await copyTreeWithoutSymlinks({ sourcePath: sourceDir, targetPath: targetDir, preserveTargetSymlinks: false, }); } async function replaceDirectoryContents({ sourceDir, targetDir, excludeDirs, }: { sourceDir: string; targetDir: string; excludeDirs?: readonly string[]; }): Promise { const excludeMatcher = createExcludeMatcher(excludeDirs); const entries = await fs.readdir(sourceDir); await Promise.all( entries.map(async (entry) => { const sourcePath = path.join(sourceDir, entry); const targetPath = path.join(targetDir, entry); if (excludeMatcher(entry)) { return; } const stats = await fs.lstat(sourcePath); if (stats.isDirectory()) { await fs.mkdir(targetPath, { recursive: true }); await replaceDirectoryContents({ sourceDir: sourcePath, targetDir: targetPath, excludeDirs, }); } else if (stats.isFile()) { await fs.copyFile(sourcePath, targetPath); } }) ); } // Changes in hackend.ts await replaceDirectoryContents({ sourceDir: tmpDir, targetDir: this.params.createParams.workspaceDir, excludeDirs: [ // Never sync hooks/ from the remote sandbox - mirrored content must not // become trusted workspace hook code on the host. "hooks", // Never sync trusted host node directories or repository metadata from // the remote sandbox ...DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS, ], }); // Changes in mirror.ts await stageDirectoryContents({ sourceDir: localPath, targetDir: tmpDir, }); const result = await runOpenShellCli({ context: this.p