Files
metona-ai-desktop/electron/harness/sandbox/sandbox.ts
T

174 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Sandbox Manager — 沙箱执行环境
*
* 四层纵深防御:进程隔离、路径白名单、网络策略、资源限制。
*
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第五章
*/
import { resolve, sep } from 'path';
import { existsSync, realpathSync } from 'fs';
export interface SandboxConfig {
allowedPaths?: string[];
networkPolicy?: 'allowall' | 'deny-all' | 'allowlist';
resourceLimits?: Partial<ResourceLimits>;
}
export interface ResourceLimits {
maxMemoryMB: number;
maxCpuSeconds: number;
maxExecutionMs: number;
maxOutputSizeKB: number;
}
export interface SandboxExecutionResult {
success: boolean;
output?: string;
stderr?: string;
exitCode?: number;
error?: string;
durationMs: number;
}
export class SandboxManager {
private allowedPaths: Set<string> = new Set();
private networkPolicy: 'allowall' | 'deny-all' | 'allowlist' = 'deny-all';
private resourceLimits: ResourceLimits = {
maxMemoryMB: 512,
maxCpuSeconds: 30,
maxExecutionMs: 60_000,
maxOutputSizeKB: 1024,
};
constructor(private config: SandboxConfig) {
this.allowedPaths = new Set(config.allowedPaths ?? []);
this.networkPolicy = config.networkPolicy ?? 'allowlist';
}
/**
* 校验文件路径是否在白名单内
*
* 安全策略:fail-closed — 如果未配置任何白名单路径,拒绝所有访问。
* 同时解析符号链接,防止通过 symlink 逃逸白名单。
*/
validatePath(requestedPath: string): { allowed: boolean; resolvedPath: string; reason?: string } {
const resolved = resolve(requestedPath);
if (this.allowedPaths.size === 0) {
return { allowed: false, resolvedPath: resolved, reason: 'No allowed paths configured (fail-closed)' };
}
// 先做字符串级白名单校验
const isAllowed = Array.from(this.allowedPaths).some(
(allowed) => resolved === allowed || resolved.startsWith(allowed + sep),
);
if (!isAllowed) {
return { allowed: false, resolvedPath: resolved, reason: 'Path not in allowed list' };
}
// 解析符号链接(如果路径存在)
if (existsSync(resolved)) {
try {
const realPath = realpathSync(resolved);
const realAllowed = Array.from(this.allowedPaths).some(
(allowed) => realPath === allowed || realPath.startsWith(allowed + sep),
);
if (!realAllowed) {
return { allowed: false, resolvedPath: realPath, reason: 'Symlink escape detected' };
}
return { allowed: true, resolvedPath: realPath };
} catch {
// realpath 解析失败(权限问题等),保守拒绝
return { allowed: false, resolvedPath: resolved, reason: 'Path resolution failed' };
}
}
return { allowed: true, resolvedPath: resolved };
}
/**
* 静态代码安全扫描
*
* 检测危险模块导入、代码执行、路径遍历、危险命令、反向 shell、
* fork bomb、PowerShell 编码执行、环境变量窃取、编码绕过等。
*/
scanCode(code: string): { safe: boolean; reason?: string } {
// C-5 修复: 补齐 28 个模式 + 加强 base64/$() 检测
// #13 修复: 补全 require('fs')、动态 import、process.binding、new Function、Reflect.get 等绕过模式
// @see project_memory.md — sandbox scanCode must include 28 patterns with 'i' flag
// and base64/$() detection to prevent encoding bypass
const dangerousPatterns = [
// 危险模块导入(3
/require\s*\(\s*['"]child_process['"]\s*\)/i,
/import\s+.*from\s+['"]fs['"]/i,
/import\s+.*from\s+['"]child_process['"]/i,
// #13 新增: require('fs') 同步 require 形式(1
/require\s*\(\s*['"]fs['"]\s*\)/i,
// #13 新增: 动态 import('fs') / import('child_process')2
/\bimport\s*\(\s*['"](?:fs|child_process|os|net|http|https|crypto|dns|cluster)['"]\s*\)/i,
// 审查修复 (M8): 兜底检测所有动态 import 调用 — 原模式只匹配字符串字面量,
// 动态 import 用变量 import(m) 可绕过。沙箱中应禁止所有动态 import。
// 注意:此模式较宽泛,会拦截 import(safeModule),但沙箱 fail-closed 策略可接受。
/\bimport\s*\(/i,
// #13 新增: process.binding('fs') 底层绑定(1
/\bprocess\.binding\s*\(/i,
// #13 新增: new Function() 函数构造(1
/\bnew\s+Function\s*\(/i,
// #13 新增: Reflect.get 字符串拼接绕过(1
/\bReflect\.get\s*\(/i,
// #13 新增: Function('return process') 等函数构造执行(1
/\bFunction\s*\(\s*['"]return\s+(?:process|require|global|globalThis)['"]\s*\)/i,
// 代码执行(3
/\beval\s*\(/i,
/process\.exit/i,
/Function\s*\(/i,
// 路径遍历(2
/\.\.\//i,
/\\\.\.\\/i, // Windows ..\
// 危险命令(3
/\brm\s+-rf\b/i,
/\bkillall\s+-9\b/i,
/\bchown\s+-R\s+\//i,
// 重定向到系统目录(2
/>\s*\/dev\/null/i,
/>\s*\/etc\//i,
// 管道执行(2
/\bcurl\b.*\|\s*(bash|sh|zsh)\b/i,
/\bwget\b.*\|\s*(sh|bash|zsh)\b/i,
// 反向 shell3
/\/bin\/(bash|sh)\s+-i/i,
/\bnc\s+-e\b/i,
/\bbash\s+-i\b/i,
// Fork bomb1
/:\(\)\s*\{\s*:\|:\s*&\s*\};:/i,
// PowerShell 编码执行(1
/powershell.*-enc(odedCommand)?\s+/i,
// 环境变量窃取(1
/env\b.*\b(GITHUB_TOKEN|API_KEY|SECRET|PASSWORD)\b/i,
// 编码绕过检测(4)— C-5 加强 base64 解码后执行 + 任意 $() 替换
// 检测 base64 解码(-d 或 --decode)后管道到 shell
/\bbase64\b.*(-d|--decode)?\b.*\|\s*(sh|bash|zsh)\b/i,
/\batob\s*\(/i,
/\bprintf\s+['"]\\x[0-9a-f]/i,
// 检测任意 $() 命令替换中包含危险命令(扩展检测范围)
/\$\([^)]*(rm|kill|del|format|mkfs|chmod|chown|curl|wget|nc|bash|sh)\b/i,
// heredoc 执行(1
/<<\s*(EOF|END)\s*[\s\S]*?\b(rm|kill|del|format|mkfs)\b/i,
// C-5 新增模式 1: Python -c 执行危险代码
/\bpython3?\b.*-c\s+['"]\s*(import\s+(os|subprocess|shutil)|exec\s*\(|eval\s*\()/i,
// C-5 新增模式 2: Node.js -e 执行危险代码
/\bnode\b.*-e\s+['"]\s*(require\s*\(\s*['"]child_process|process\.exit|execSync|spawnSync)/i,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(code)) {
// 不暴露 pattern.source,使用通用错误信息
return { safe: false, reason: 'Command blocked by security policy' };
}
}
return { safe: true };
}
}