# OpenShell 漏洞总结 ## 漏洞概述 OpenShell 是一个允许在沙盒环境中运行任意代码的工具。该漏洞涉及沙盒边界被绕过,导致恶意代码可以在沙盒外执行。具体表现为: - 沙盒同步边界被硬化,防止恶意代码逃逸。 - 沙盒测试被优化,确保沙盒的完整性。 - 受信任的镜像符号链接被保留,防止恶意符号链接攻击。 - 全局镜像文件系统边界被绑定,防止跨文件系统攻击。 ## 影响范围 - **受影响版本**:v2026.4.26 至 v2026.3.31-beta.1 - **影响组件**:OpenShell 的沙盒机制 - **潜在风险**:恶意代码可能在沙盒外执行,导致系统被完全控制。 ## 修复方案 - **代码变更**: - 在 `extensions/openshell/src/hackend.ts` 中,增加了沙盒同步边界的硬化措施,防止恶意代码逃逸。 - 在 `extensions/openshell/src/mirror.ts` 中,优化了沙盒测试,确保沙盒的完整性。 - 在 `extensions/openshell/src/mirror.ts` 中,保留了受信任的镜像符号链接,防止恶意符号链接攻击。 - 在 `extensions/openshell/src/mirror.ts` 中,绑定了全局镜像文件系统边界,防止跨文件系统攻击。 ### 关键代码变更 ```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); } }) ); } // 在 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, ], }); // 在 mirror.ts 中的变更 await stageDirectoryContents({ sourceDir: localPath, targetDir: tmpDir, }); const result = await runOpenShellCli({ context: this.params.execContext, args: [ "sandbox", "upload", "--no-git-ignore", this.params.execContext.sandboxName, tmpDir, remotePath, ], cwd: this.params.createParams.workspaceDir, }); if (result.code !== 0) { throw new Error(result.stderr.trim()) || "openshell sandbox upload failed"; } ``` ## 总结 该漏洞通过硬化沙盒边界、优化沙盒测试、保留受信任的镜像符号链接和绑定全局镜像文件系统边界来修复,确保恶意代码无法在沙盒外执行。