fix(core): 修复 Plan Agent 通过 heredoc 绕过写入限制的安全漏洞
- 重排 bash 权限规则顺序,deny 规则置于 allow 规则之前 - 添加 heredoc 重定向检测规则 (* << *) - 新增 checkRedirectInRawCommand 预检函数,在 tree-sitter 解析前检测重定向 - 禁用 Plan Agent 的 tool_search 工具,防止动态发现写入工具 - 添加更多危险命令: ln, install, truncate, dd, tee
This commit is contained in:
@@ -37,6 +37,8 @@ export const planAgent: Omit<AgentInfo, 'name'> = {
|
|||||||
'checkpoint_create',
|
'checkpoint_create',
|
||||||
'checkpoint_restore',
|
'checkpoint_restore',
|
||||||
'undo',
|
'undo',
|
||||||
|
// 工具发现(Plan 模式不应动态发现新工具)
|
||||||
|
'tool_search',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
permission: {
|
permission: {
|
||||||
@@ -49,6 +51,52 @@ export const planAgent: Omit<AgentInfo, 'name'> = {
|
|||||||
bash: {
|
bash: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
rules: [
|
rules: [
|
||||||
|
// ============================================================
|
||||||
|
// 重要:deny 规则必须放在 allow 规则之前,确保优先匹配!
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// ============ 危险命令 - 拒绝 ============
|
||||||
|
{ pattern: 'rm *', action: 'deny' },
|
||||||
|
{ pattern: 'rmdir *', action: 'deny' },
|
||||||
|
{ pattern: 'mv *', action: 'deny' },
|
||||||
|
{ pattern: 'cp *', action: 'deny' },
|
||||||
|
{ pattern: 'mkdir *', action: 'deny' },
|
||||||
|
{ pattern: 'touch *', action: 'deny' },
|
||||||
|
{ pattern: 'chmod *', action: 'deny' },
|
||||||
|
{ pattern: 'chown *', action: 'deny' },
|
||||||
|
{ pattern: 'sudo *', action: 'deny' },
|
||||||
|
{ pattern: 'su *', action: 'deny' },
|
||||||
|
{ pattern: 'ln *', action: 'deny' },
|
||||||
|
{ pattern: 'install *', action: 'deny' },
|
||||||
|
{ pattern: 'truncate *', action: 'deny' },
|
||||||
|
{ pattern: 'dd *', action: 'deny' },
|
||||||
|
{ pattern: 'tee *', action: 'deny' },
|
||||||
|
|
||||||
|
// ============ 重定向操作 - 拒绝(必须在 cat/echo 等允许规则之前)============
|
||||||
|
{ pattern: '* > *', action: 'deny' },
|
||||||
|
{ pattern: '* >> *', action: 'deny' },
|
||||||
|
{ pattern: '* << *', action: 'deny' }, // heredoc 重定向
|
||||||
|
|
||||||
|
// ============ Git 写操作 - 拒绝 ============
|
||||||
|
{ pattern: 'git add *', action: 'deny' },
|
||||||
|
{ pattern: 'git commit *', action: 'deny' },
|
||||||
|
{ pattern: 'git push *', action: 'deny' },
|
||||||
|
{ pattern: 'git pull *', action: 'deny' },
|
||||||
|
{ pattern: 'git checkout *', action: 'deny' },
|
||||||
|
{ pattern: 'git reset *', action: 'deny' },
|
||||||
|
{ pattern: 'git rebase *', action: 'deny' },
|
||||||
|
{ pattern: 'git merge *', action: 'deny' },
|
||||||
|
{ pattern: 'git stash *', action: 'deny' },
|
||||||
|
{ pattern: 'git clean *', action: 'deny' },
|
||||||
|
|
||||||
|
// ============ find 危险操作 - 拒绝 ============
|
||||||
|
{ pattern: 'find * -delete*', action: 'deny' },
|
||||||
|
{ pattern: 'find * -exec*', action: 'deny' },
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 以下为只读操作的 allow 规则
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
// ============ 文件查看 - 允许 ============
|
// ============ 文件查看 - 允许 ============
|
||||||
{ pattern: 'ls', action: 'allow' },
|
{ pattern: 'ls', action: 'allow' },
|
||||||
{ pattern: 'ls *', action: 'allow' },
|
{ pattern: 'ls *', action: 'allow' },
|
||||||
@@ -100,38 +148,6 @@ export const planAgent: Omit<AgentInfo, 'name'> = {
|
|||||||
{ pattern: 'git blame *', action: 'allow' },
|
{ pattern: 'git blame *', action: 'allow' },
|
||||||
{ pattern: 'git ls-files*', action: 'allow' },
|
{ pattern: 'git ls-files*', action: 'allow' },
|
||||||
{ pattern: 'git rev-parse *', action: 'allow' },
|
{ pattern: 'git rev-parse *', action: 'allow' },
|
||||||
|
|
||||||
// ============ 危险命令 - 拒绝 ============
|
|
||||||
{ pattern: 'rm *', action: 'deny' },
|
|
||||||
{ pattern: 'rmdir *', action: 'deny' },
|
|
||||||
{ pattern: 'mv *', action: 'deny' },
|
|
||||||
{ pattern: 'cp *', action: 'deny' },
|
|
||||||
{ pattern: 'mkdir *', action: 'deny' },
|
|
||||||
{ pattern: 'touch *', action: 'deny' },
|
|
||||||
{ pattern: 'chmod *', action: 'deny' },
|
|
||||||
{ pattern: 'chown *', action: 'deny' },
|
|
||||||
{ pattern: 'sudo *', action: 'deny' },
|
|
||||||
{ pattern: 'su *', action: 'deny' },
|
|
||||||
|
|
||||||
// ============ Git 写操作 - 拒绝 ============
|
|
||||||
{ pattern: 'git add *', action: 'deny' },
|
|
||||||
{ pattern: 'git commit *', action: 'deny' },
|
|
||||||
{ pattern: 'git push *', action: 'deny' },
|
|
||||||
{ pattern: 'git pull *', action: 'deny' },
|
|
||||||
{ pattern: 'git checkout *', action: 'deny' },
|
|
||||||
{ pattern: 'git reset *', action: 'deny' },
|
|
||||||
{ pattern: 'git rebase *', action: 'deny' },
|
|
||||||
{ pattern: 'git merge *', action: 'deny' },
|
|
||||||
{ pattern: 'git stash *', action: 'deny' },
|
|
||||||
{ pattern: 'git clean *', action: 'deny' },
|
|
||||||
|
|
||||||
// ============ find 危险操作 - 拒绝 ============
|
|
||||||
{ pattern: 'find * -delete*', action: 'deny' },
|
|
||||||
{ pattern: 'find * -exec*', action: 'deny' },
|
|
||||||
|
|
||||||
// ============ 重定向操作 - 拒绝 ============
|
|
||||||
{ pattern: '* > *', action: 'deny' },
|
|
||||||
{ pattern: '* >> *', action: 'deny' },
|
|
||||||
],
|
],
|
||||||
default: 'ask', // 其他命令询问用户
|
default: 'ask', // 其他命令询问用户
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -95,6 +95,35 @@ export function matchRules(
|
|||||||
return matchSingleCommand(parsed, rules, defaultAction);
|
return matchSingleCommand(parsed, rules, defaultAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查原始命令字符串是否包含危险的重定向操作
|
||||||
|
* 这是一个快速预检,在 tree-sitter 解析之前进行
|
||||||
|
*/
|
||||||
|
function checkRedirectInRawCommand(
|
||||||
|
command: string,
|
||||||
|
rules: PermissionRule[]
|
||||||
|
): { action: PermissionAction; matchedPattern?: string } | null {
|
||||||
|
// 检查重定向相关的 deny 规则
|
||||||
|
const redirectPatterns = ['* > *', '* >> *', '* << *'];
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule.action === 'deny' && redirectPatterns.includes(rule.pattern)) {
|
||||||
|
// 根据模式检查原始命令
|
||||||
|
if (rule.pattern === '* > *' && / > /.test(command) && !/ >> /.test(command)) {
|
||||||
|
return { action: 'deny', matchedPattern: rule.pattern };
|
||||||
|
}
|
||||||
|
if (rule.pattern === '* >> *' && / >> /.test(command)) {
|
||||||
|
return { action: 'deny', matchedPattern: rule.pattern };
|
||||||
|
}
|
||||||
|
if (rule.pattern === '* << *' && / << /.test(command)) {
|
||||||
|
return { action: 'deny', matchedPattern: rule.pattern };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用 tree-sitter 解析并检查所有命令的权限(异步版本)
|
* 使用 tree-sitter 解析并检查所有命令的权限(异步版本)
|
||||||
* 返回最严格的权限要求
|
* 返回最严格的权限要求
|
||||||
@@ -109,6 +138,18 @@ export async function matchRulesAsync(
|
|||||||
allCommands: ParsedCommand[];
|
allCommands: ParsedCommand[];
|
||||||
askPatterns: string[];
|
askPatterns: string[];
|
||||||
}> {
|
}> {
|
||||||
|
// 首先检查原始命令中的重定向操作(这是安全关键检查,必须在解析之前进行)
|
||||||
|
const redirectCheck = checkRedirectInRawCommand(command, rules);
|
||||||
|
if (redirectCheck) {
|
||||||
|
// 如果检测到重定向被拒绝,直接返回
|
||||||
|
return {
|
||||||
|
action: redirectCheck.action,
|
||||||
|
matchedPattern: redirectCheck.matchedPattern,
|
||||||
|
allCommands: [parseCommandSimple(command)],
|
||||||
|
askPatterns: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const result = await parseBashCommand(command);
|
const result = await parseBashCommand(command);
|
||||||
|
|
||||||
// 如果解析失败,降级到简单解析
|
// 如果解析失败,降级到简单解析
|
||||||
|
|||||||
Reference in New Issue
Block a user