This commit is contained in:
lik
2026-09-09 17:36:45 +08:00
parent d6efb180dd
commit 223e0ed2ee
25 changed files with 933 additions and 59 deletions
+96
View File
@@ -0,0 +1,96 @@
import path from "path";
import { FilesystemBackend } from "deepagents";
/**
* 受限文件系统后端:在 FilesystemBackend 基础上收紧 LLM 文件工具的访问范围。
*
* 相比原生非 virtualMode 模式的两个安全缺口:
* 1. 原生 resolvePath 对绝对路径原样放行(可读磁盘任意文件),此处强制锁定在 rootDir 内;
* 2. 沙箱根下的敏感文件(conf.json、.env*、logs、dataDir)默认禁止访问,
* 防止 LLM 通过 read_file/grep 读取密钥、数据库连接串及其他用户记忆。
*
* 注意:memoryAGENTS.md)与 skillsSKILL.md)也经由 backend 加载,
* 均不在 deny 列表中,不受影响。
*
* 注:1.11+ 的 createDeepAgent permissions 机制对真实磁盘后端不适用——
* 非虚拟模式下 ls/glob/grep 结果是 Windows 绝对路径,而权限规则强制以 "/" 开头,
* 永远匹配不上(grep 会泄露 conf.json 内容)。敏感路径拦截必须保持在后端层实现。
*/
export class RestrictedFilesystemBackend extends FilesystemBackend {
constructor(options = {}) {
super(options);
this.denyPaths = (options.deny ?? ["conf.json", "logs"]).map(d => String(d).toLowerCase().replace(/\/+$/, ""));
}
// 判断已解析的绝对路径是否命中 deny 列表(目录条目连同其子路径一并命中)
_isDenied(fullPath) {
const rel = path.relative(this.cwd, fullPath);
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return false;
const norm = rel.split(path.sep).join("/").toLowerCase();
// 根目录下的 .env / .env.local 等
if (norm === ".env" || norm.startsWith(".env.")) return true;
return this.denyPaths.some(d => norm === d || norm.startsWith(d + "/"));
}
/**
* 覆写路径解析:一律锁定在 rootDir 内。
* - "/" 或空串视为沙箱根(LLM 工具以 / 为根)
* - 相对路径按 rootDir 解析
* - 绝对路径仅接受位于 rootDir 内的(非 virtualMode 的 ls/glob 返回的是绝对路径,需支持回传)
* - 禁止 .. 与 ~ 穿越
* - 命中 deny 列表直接抛错
*/
resolvePath(key) {
const k = String(key);
if (k.includes("..") || k.startsWith("~")) {
throw new Error(`Path traversal not allowed: ${key}`);
}
let full;
if (/^\/+$/.test(k)) {
full = this.cwd;
} else if (path.isAbsolute(k)) {
full = path.resolve(k);
} else {
full = path.resolve(this.cwd, k);
}
const rel = path.relative(this.cwd, full);
if (rel.startsWith("..") || path.isAbsolute(rel)) {
throw new Error(`Path outside sandbox root: ${key}`);
}
if (this._isDenied(full)) {
throw new Error(`Access denied: ${key}`);
}
return full;
}
// ls/glob/grep 需过滤结果,避免泄露敏感文件的存在性与内容
async ls(dirPath) {
const result = await super.ls(dirPath);
return { files: (result.files ?? []).filter(f => !this._isDenied(f.path)) };
}
async glob(pattern, searchPath = "/") {
const result = await super.glob(pattern, searchPath);
return { files: (result.files ?? []).filter(f => !this._isDenied(f.path)) };
}
async grep(pattern, dirPath = "/", globFilter = null, maxCount = null) {
const result = await super.grep(pattern, dirPath, globFilter, maxCount);
return { ...result, matches: (result.matches ?? []).filter(m => !this._isDenied(m.path)) };
}
}
/**
* 创建 bot 专用的受限后端,自动把 dataDirconf.json agent.dataDir)加入 deny 列表。
* dataDir 位于沙箱根之外时无需 deny(沙箱本身不可达)。
*/
export function createRestrictedBackend({ rootDir, dataDir }) {
const deny = ["conf.json", "logs"];
if (dataDir) {
const rel = path.relative(path.resolve(rootDir), path.resolve(dataDir));
if (rel && !rel.startsWith("..") && !path.isAbsolute(rel)) {
deny.push(rel.split(path.sep).join("/"));
}
}
return new RestrictedFilesystemBackend({ rootDir, deny });
}