@raolin2025/claude-code-node 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/package.json +13 -4
- package/src/channel/index.js +81 -146
- package/src/channel/notify-daemon.js +592 -0
- package/src/core/cli.js +195 -114
- package/src/core/compact.js +171 -0
- package/src/core/config.js +1 -2
- package/src/core/cost-tracker.js +171 -0
- package/src/core/paths.js +12 -0
- package/src/core/query-engine.js +192 -89
- package/src/core/session.js +24 -9
- package/src/mcp/client.js +99 -1
- package/src/mcp/registry.js +1 -2
- package/src/security/bash-guard.js +174 -141
- package/src/security/enhanced-permission.js +72 -34
- package/src/security/path-guard.js +32 -29
- package/src/security/ssrf-guard.js +153 -50
- package/src/tools/glob.js +1 -1
- package/src/types/index.js +2 -1
- package/src/utils/file-ops.js +2 -3
package/src/mcp/client.js
CHANGED
|
@@ -2,8 +2,93 @@
|
|
|
2
2
|
* MCP (Model Context Protocol) 客户端
|
|
3
3
|
* 对应原版: src/services/mcp/
|
|
4
4
|
* 简化版:支持 stdio 传输,JSON-RPC 2.0
|
|
5
|
+
*
|
|
6
|
+
* v1.1 修复:
|
|
7
|
+
* - 新增 MCP 服务器命令白名单,阻止任意命令执行
|
|
8
|
+
* - 新增沙箱环境变量清理
|
|
9
|
+
* - 新增 spawn 参数验证
|
|
5
10
|
*/
|
|
6
11
|
import { spawn } from 'child_process'
|
|
12
|
+
import { resolve, basename } from 'path'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* MCP 服务器命令白名单
|
|
16
|
+
* 只允许这些可执行文件名(不含路径)
|
|
17
|
+
* 阻止: /bin/bash, /bin/sh, python, node, perl, ruby 等通用解释器
|
|
18
|
+
*/
|
|
19
|
+
const ALLOWED_MCP_COMMANDS = [
|
|
20
|
+
'npx', // Node.js 包执行器
|
|
21
|
+
'uvx', // Python uv 执行器
|
|
22
|
+
'mcp-server', // 通用 MCP 服务器前缀
|
|
23
|
+
'mcp-', // mcp- 前缀的服务器
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 明确禁止的命令 — 无论白名单如何都不允许
|
|
28
|
+
*/
|
|
29
|
+
const BLOCKED_MCP_COMMANDS = [
|
|
30
|
+
'bash', 'sh', 'zsh', 'fish', 'dash', 'ksh', 'csh', 'tcsh',
|
|
31
|
+
'python', 'python2', 'python3',
|
|
32
|
+
'perl', 'ruby', 'php',
|
|
33
|
+
'node', 'deno', 'bun',
|
|
34
|
+
'nc', 'ncat', 'socat', 'telnet',
|
|
35
|
+
'curl', 'wget',
|
|
36
|
+
'eval', 'exec', 'source',
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 验证 MCP 服务器命令是否安全
|
|
41
|
+
* @param {string} command — 要执行的命令
|
|
42
|
+
* @returns {{allowed: boolean, reason?: string}}
|
|
43
|
+
*/
|
|
44
|
+
function validateMcpCommand(command) {
|
|
45
|
+
// 提取命令的基本名(去除路径)
|
|
46
|
+
const cmdBase = basename(command).toLowerCase()
|
|
47
|
+
|
|
48
|
+
// 1. 检查是否在禁止列表中
|
|
49
|
+
if (BLOCKED_MCP_COMMANDS.includes(cmdBase)) {
|
|
50
|
+
return { allowed: false, reason: `MCP 服务器命令禁止: ${cmdBase}(通用解释器/网络工具不允许作为 MCP 服务器)` }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. 检查绝对路径中的可疑位置
|
|
54
|
+
if (command.includes('/')) {
|
|
55
|
+
const resolved = resolve(command)
|
|
56
|
+
// 阻止 /tmp, /dev/shm 等临时目录
|
|
57
|
+
if (resolved.startsWith('/tmp/') || resolved.startsWith('/dev/shm/') || resolved.startsWith('/var/tmp/')) {
|
|
58
|
+
return { allowed: false, reason: `MCP 服务器命令在临时目录: ${resolved}(可能为注入攻击)` }
|
|
59
|
+
}
|
|
60
|
+
// 阻止 /dev, /proc, /sys
|
|
61
|
+
if (resolved.startsWith('/dev/') || resolved.startsWith('/proc/') || resolved.startsWith('/sys/')) {
|
|
62
|
+
return { allowed: false, reason: `MCP 服务器命令在系统目录: ${resolved}` }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. 检查是否匹配白名单前缀
|
|
67
|
+
const isAllowed = ALLOWED_MCP_COMMANDS.some(prefix => cmdBase.startsWith(prefix))
|
|
68
|
+
if (!isAllowed) {
|
|
69
|
+
return { allowed: false, reason: `MCP 服务器命令不在白名单中: ${cmdBase}(允许前缀: ${ALLOWED_MCP_COMMANDS.join(', ')})` }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { allowed: true }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 清理环境变量 — 移除敏感变量,防止注入
|
|
77
|
+
*/
|
|
78
|
+
function sanitizeEnv(env) {
|
|
79
|
+
const sanitized = { ...process.env, ...env }
|
|
80
|
+
// 移除可能导致注入的环境变量
|
|
81
|
+
const dangerousEnvKeys = [
|
|
82
|
+
'LD_PRELOAD', 'LD_LIBRARY_PATH',
|
|
83
|
+
'PYTHONPATH', 'PYTHONSTARTUP',
|
|
84
|
+
'NODE_OPTIONS',
|
|
85
|
+
'BASH_ENV', 'ENV',
|
|
86
|
+
]
|
|
87
|
+
for (const key of dangerousEnvKeys) {
|
|
88
|
+
delete sanitized[key]
|
|
89
|
+
}
|
|
90
|
+
return sanitized
|
|
91
|
+
}
|
|
7
92
|
|
|
8
93
|
/**
|
|
9
94
|
* JSON-RPC 2.0 请求 ID 计数器
|
|
@@ -31,9 +116,22 @@ export class MCPClient {
|
|
|
31
116
|
async connect() {
|
|
32
117
|
const { command, args = [], env = {} } = this.config
|
|
33
118
|
|
|
119
|
+
// v1.1: 验证命令安全性
|
|
120
|
+
const validation = validateMcpCommand(command)
|
|
121
|
+
if (!validation.allowed) {
|
|
122
|
+
throw new Error(`MCP 服务器命令验证失败: ${validation.reason}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// v1.1: 验证参数不包含注入
|
|
126
|
+
for (const arg of args) {
|
|
127
|
+
if (typeof arg !== 'string') {
|
|
128
|
+
throw new Error(`MCP 服务器参数类型错误: ${typeof arg},期望 string`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
34
132
|
this.process = spawn(command, args, {
|
|
35
133
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
36
|
-
env:
|
|
134
|
+
env: sanitizeEnv(env),
|
|
37
135
|
})
|
|
38
136
|
|
|
39
137
|
this.process.stdout.on('data', (data) => {
|
package/src/mcp/registry.js
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
* 对应原版: src/services/mcp/mcpServerApproval.tsx + 配置加载
|
|
4
4
|
*/
|
|
5
5
|
import { readFile, writeFile, mkdir } from 'fs/promises'
|
|
6
|
-
import { resolve
|
|
7
|
-
import { existsSync } from 'fs'
|
|
6
|
+
import { resolve } from 'path'
|
|
8
7
|
import { MCPClient } from './client.js'
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* BashTool 命令安全检查
|
|
3
3
|
* 对应原版: src/tools/BashTool/bashSecurity.ts (2592行简化版)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* v1.1 修复:
|
|
6
|
+
* - 新增危险命令: mount, umount, chown, chroot, chgrp, mkswap, swapoff, swapon
|
|
7
|
+
* - 新增命令替换注入检测: $(cmd) 和 `cmd` 在高危上下文中阻止
|
|
8
|
+
* - 新增进程注入检测: /proc/self 内存操作
|
|
9
|
+
* - 新增环境变量注入检测: LD_PRELOAD, LD_LIBRARY_PATH
|
|
10
|
+
* - 强化管道注入: curl/wget 到任意端口 + 管道执行
|
|
11
|
+
* - 修复 splitPipeSegments: 正确处理引号内的 |
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -18,152 +18,129 @@
|
|
|
18
18
|
*/
|
|
19
19
|
const DANGEROUS_PATTERNS = [
|
|
20
20
|
// === 破坏性操作 ===
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
severity: 'critical',
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
pattern: /\bformat\s+[A-Z]:/i,
|
|
43
|
-
reason: 'Windows 格式化磁盘',
|
|
44
|
-
severity: 'critical',
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
pattern: /:\(\)\{\s*:\|\:&\s*\}\s*;/,
|
|
48
|
-
reason: 'Fork bomb(fork 炸弹)',
|
|
49
|
-
severity: 'critical',
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
pattern: />\s*\/dev\/sda/,
|
|
53
|
-
reason: '直接写入块设备',
|
|
54
|
-
severity: 'critical',
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
pattern: /\bchmod\s+([0-7]{3,4}|[ugo]*[+-][rwx].*)\s+\/(etc|boot|usr)\b/,
|
|
58
|
-
reason: '修改系统目录权限',
|
|
59
|
-
severity: 'critical',
|
|
60
|
-
},
|
|
21
|
+
{ pattern: /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?\/\s*$/, reason: '递归删除根目录', severity: 'critical' },
|
|
22
|
+
{ pattern: /\brm\s+.*-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*.*\s\/[a-zA-Z]*/, reason: '递归强制删除系统目录', severity: 'critical' },
|
|
23
|
+
{ pattern: /\bdd\s+if=.*of=\/dev\//, reason: 'dd 写入设备文件', severity: 'critical' },
|
|
24
|
+
{ pattern: /\bmkfs\b/, reason: '格式化文件系统', severity: 'critical' },
|
|
25
|
+
{ pattern: /\bformat\s+[A-Z]:/i, reason: 'Windows 格式化磁盘', severity: 'critical' },
|
|
26
|
+
{ pattern: /:\(\)\{\s*:\|\:&\s*\}\s*;/, reason: 'Fork bomb(fork 炸弹)', severity: 'critical' },
|
|
27
|
+
{ pattern: />\s*\/dev\/sda/, reason: '直接写入块设备', severity: 'critical' },
|
|
28
|
+
{ pattern: /\bchmod\s+([0-7]{3,4}|[ugo]*[+-][rwx].*)\s+\/(etc|boot|usr)\b/, reason: '修改系统目录权限', severity: 'critical' },
|
|
29
|
+
|
|
30
|
+
// === v1.1 新增: 系统级破坏命令 ===
|
|
31
|
+
{ pattern: /\bmount\b/, reason: '挂载文件系统(可能修改系统分区)', severity: 'critical' },
|
|
32
|
+
{ pattern: /\bumount\b/, reason: '卸载文件系统(可能导致数据丢失)', severity: 'high' },
|
|
33
|
+
{ pattern: /\bchown\b.*\/(etc|boot|usr|root)\b/, reason: '修改系统目录所有者', severity: 'critical' },
|
|
34
|
+
{ pattern: /\bchgrp\b.*\/(etc|boot|usr|root)\b/, reason: '修改系统目录组', severity: 'high' },
|
|
35
|
+
{ pattern: /\bchroot\b/, reason: 'chroot 改变根目录(逃逸风险)', severity: 'high' },
|
|
36
|
+
{ pattern: /\bmkswap\b/, reason: '创建交换分区', severity: 'critical' },
|
|
37
|
+
{ pattern: /\bswapoff\b/, reason: '禁用交换分区', severity: 'high' },
|
|
38
|
+
{ pattern: /\bswapon\b/, reason: '启用交换分区', severity: 'high' },
|
|
61
39
|
|
|
62
40
|
// === 敏感文件访问 ===
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
reason: '访问 SSH 私钥/配置',
|
|
71
|
-
severity: 'high',
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
pattern: /\bcat\b.*\/etc\/shadow/,
|
|
75
|
-
reason: '读取 shadow 密码文件',
|
|
76
|
-
severity: 'critical',
|
|
77
|
-
},
|
|
41
|
+
{ pattern: /\/etc\/(shadow|passwd|sudoers|ssh\/sshd_config|gshadow|pam\.d)\b/, reason: '访问敏感系统文件', severity: 'high' },
|
|
42
|
+
{ pattern: /~\/\.ssh\/(id_[a-z]+|authorized_keys|config)\b/, reason: '访问 SSH 私钥/配置', severity: 'high' },
|
|
43
|
+
{ pattern: /\bcat\b.*\/etc\/shadow/, reason: '读取 shadow 密码文件', severity: 'critical' },
|
|
44
|
+
|
|
45
|
+
// === v1.1 新增: /proc/self 内存操作 ===
|
|
46
|
+
{ pattern: /\/proc\/self\/(mem|environ|maps|auxv)/, reason: '访问 /proc/self 敏感文件(内存泄露/逃逸)', severity: 'critical' },
|
|
47
|
+
{ pattern: /\/proc\/sys\/kernel\/(core_pattern|modprobe|panic|hostname)/, reason: '修改内核参数(容器逃逸)', severity: 'critical' },
|
|
78
48
|
|
|
79
49
|
// === 网络数据外泄 ===
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
severity: 'critical',
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
pattern: /\b(iex|Invoke-Expression)\b.*\b(Invoke-WebRequest|iwr|New-Object.*WebClient)\b/i,
|
|
92
|
-
reason: 'PowerShell 下载并执行',
|
|
93
|
-
severity: 'critical',
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
pattern: /\bpython[23]?\s+-c\s+.*import\s+(urllib|requests|http\.client|socket)/,
|
|
97
|
-
reason: 'Python 内联网络请求',
|
|
98
|
-
severity: 'high',
|
|
99
|
-
},
|
|
50
|
+
{ pattern: /\bcurl\b.*\|\s*(bash|sh|zsh|fish)\b/, reason: '从网络下载并执行脚本(curl | bash)', severity: 'critical' },
|
|
51
|
+
{ pattern: /\bwget\b.*\|\s*(bash|sh|zsh|fish)\b/, reason: '从网络下载并执行脚本(wget | sh)', severity: 'critical' },
|
|
52
|
+
{ pattern: /\b(iex|Invoke-Expression)\b.*\b(Invoke-WebRequest|iwr|New-Object.*WebClient)\b/i, reason: 'PowerShell 下载并执行', severity: 'critical' },
|
|
53
|
+
{ pattern: /\bpython[23]?\s+-c\s+.*import\s+(urllib|requests|http\.client|socket)/, reason: 'Python 内联网络请求', severity: 'high' },
|
|
54
|
+
|
|
55
|
+
// === v1.1 新增: 管道 + shell 执行 ===
|
|
56
|
+
{ pattern: /\bcurl\b.*--exec\b/, reason: 'curl --exec 下载并执行', severity: 'critical' },
|
|
57
|
+
{ pattern: /\bcurl\b.*\bxargs\b.*\b(bash|sh|zsh)\b/, reason: 'curl 下载 + xargs 执行', severity: 'critical' },
|
|
100
58
|
|
|
101
59
|
// === 提权/权限逃逸 ===
|
|
102
|
-
{
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
severity: 'critical',
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
pattern: /\bpkexec\b/,
|
|
114
|
-
reason: 'PolicyKit 提权',
|
|
115
|
-
severity: 'high',
|
|
116
|
-
},
|
|
60
|
+
{ pattern: /\bsudo\s+su\b/, reason: '切换到 root 用户', severity: 'high' },
|
|
61
|
+
{ pattern: /\bsudo\s+chmod\s+[0-7]{3,4}\s+\/(etc|usr|boot)\b/, reason: 'sudo 修改系统目录权限', severity: 'critical' },
|
|
62
|
+
{ pattern: /\bpkexec\b/, reason: 'PolicyKit 提权', severity: 'high' },
|
|
63
|
+
|
|
64
|
+
// === v1.1 新增: 环境变量注入 ===
|
|
65
|
+
{ pattern: /\bLD_PRELOAD\s*=/, reason: 'LD_PRELOAD 注入(劫持动态链接库)', severity: 'critical' },
|
|
66
|
+
{ pattern: /\bLD_LIBRARY_PATH\s*=/, reason: 'LD_LIBRARY_PATH 注入(劫持库搜索路径)', severity: 'high' },
|
|
67
|
+
{ pattern: /\bPYTHONPATH\s*=/, reason: 'PYTHONPATH 注入(劫持 Python 模块搜索)', severity: 'high' },
|
|
117
68
|
|
|
118
69
|
// === 容器/云逃逸 ===
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
{
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
severity: 'critical',
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
pattern: /\bdocker\s+(run|exec).*--privileged/,
|
|
131
|
-
reason: '启动特权容器',
|
|
132
|
-
severity: 'high',
|
|
133
|
-
},
|
|
70
|
+
{ pattern: /\bnsenter\b.*--target\s+1\b/, reason: '容器 namespace 逃逸到 PID 1', severity: 'critical' },
|
|
71
|
+
{ pattern: /\/proc\/sys\/kernel\/core_pattern/, reason: '修改 core_pattern(容器逃逸技术)', severity: 'critical' },
|
|
72
|
+
{ pattern: /\bdocker\s+(run|exec).*--privileged/, reason: '启动特权容器', severity: 'high' },
|
|
73
|
+
|
|
74
|
+
// === v1.1 新增: 内核模块 ===
|
|
75
|
+
{ pattern: /\binsmod\b/, reason: '加载内核模块', severity: 'critical' },
|
|
76
|
+
{ pattern: /\brmmod\b/, reason: '卸载内核模块', severity: 'high' },
|
|
77
|
+
{ pattern: /\bmodprobe\b/, reason: '自动加载内核模块', severity: 'high' },
|
|
134
78
|
]
|
|
135
79
|
|
|
80
|
+
/**
|
|
81
|
+
* 命令替换注入检测
|
|
82
|
+
* 检查 $(cmd) 和 `cmd` 在高危上下文中的使用
|
|
83
|
+
*/
|
|
84
|
+
function checkCommandSubstitution(command) {
|
|
85
|
+
const findings = []
|
|
86
|
+
|
|
87
|
+
// 检测 $() 命令替换
|
|
88
|
+
const subshellPattern = /\$\([^)]*\)/g
|
|
89
|
+
let match
|
|
90
|
+
while ((match = subshellPattern.exec(command)) !== null) {
|
|
91
|
+
const subCmd = match[0]
|
|
92
|
+
// 检查子命令中是否包含危险操作
|
|
93
|
+
for (const { pattern, reason, severity } of DANGEROUS_PATTERNS) {
|
|
94
|
+
if (pattern.test(subCmd)) {
|
|
95
|
+
findings.push({ blocked: true, reason: `命令替换注入: ${subCmd} 包含 ${reason}`, severity })
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 检查子命令中的网络请求(SSRF via 命令替换)
|
|
99
|
+
if (/\b(curl|wget|fetch|nc|ncat|socat)\b/.test(subCmd)) {
|
|
100
|
+
findings.push({ blocked: true, reason: `命令替换中包含网络请求: ${subCmd}`, severity: 'high' })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 检测反引号命令替换
|
|
105
|
+
const backtickPattern = /`[^`]+`/g
|
|
106
|
+
while ((match = backtickPattern.exec(command)) !== null) {
|
|
107
|
+
const subCmd = match[0]
|
|
108
|
+
for (const { pattern, reason, severity } of DANGEROUS_PATTERNS) {
|
|
109
|
+
if (pattern.test(subCmd)) {
|
|
110
|
+
findings.push({ blocked: true, reason: `反引号命令替换注入: ${subCmd} 包含 ${reason}`, severity })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (/\b(curl|wget|fetch|nc|ncat|socat)\b/.test(subCmd)) {
|
|
114
|
+
findings.push({ blocked: true, reason: `反引号命令替换中包含网络请求: ${subCmd}`, severity: 'high' })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return findings
|
|
119
|
+
}
|
|
120
|
+
|
|
136
121
|
/**
|
|
137
122
|
* 分段分析 — 检查跨管道段的 cd+git 组合
|
|
138
123
|
*/
|
|
139
124
|
function checkCrossSegmentCdGit(segments) {
|
|
140
125
|
let hasCd = false
|
|
141
126
|
let hasGit = false
|
|
142
|
-
|
|
143
127
|
for (const segment of segments) {
|
|
144
128
|
const trimmed = segment.trim()
|
|
145
|
-
// cd 检测
|
|
146
129
|
if (/^\bcd\s+/.test(trimmed) || /\&\&\s*cd\s+/.test(trimmed) || /\|\s*cd\s+/.test(trimmed)) {
|
|
147
130
|
hasCd = true
|
|
148
131
|
}
|
|
149
|
-
// git 检测
|
|
150
132
|
if (/\bgit\s+/.test(trimmed)) {
|
|
151
133
|
hasGit = true
|
|
152
134
|
}
|
|
153
135
|
}
|
|
154
|
-
|
|
155
136
|
if (hasCd && hasGit) {
|
|
156
|
-
return {
|
|
157
|
-
blocked: true,
|
|
158
|
-
reason: 'cd + git 组合命令:可能利用裸仓库 fsmonitor 绕过安全检查',
|
|
159
|
-
}
|
|
137
|
+
return { blocked: true, reason: 'cd + git 组合命令:可能利用裸仓库 fsmonitor 绕过安全检查' }
|
|
160
138
|
}
|
|
161
|
-
|
|
162
139
|
return { blocked: false }
|
|
163
140
|
}
|
|
164
141
|
|
|
165
142
|
/**
|
|
166
|
-
* 多 cd 命令检测
|
|
143
|
+
* 多 cd 命令检测
|
|
167
144
|
*/
|
|
168
145
|
function checkMultipleCd(segments) {
|
|
169
146
|
let cdCount = 0
|
|
@@ -174,20 +151,69 @@ function checkMultipleCd(segments) {
|
|
|
174
151
|
}
|
|
175
152
|
}
|
|
176
153
|
if (cdCount > 1) {
|
|
177
|
-
return {
|
|
178
|
-
blocked: true,
|
|
179
|
-
reason: `一条命令中包含 ${cdCount} 次 cd,需要确认以避免混淆`,
|
|
180
|
-
}
|
|
154
|
+
return { blocked: true, reason: `一条命令中包含 ${cdCount} 次 cd,需要确认以避免混淆` }
|
|
181
155
|
}
|
|
182
156
|
return { blocked: false }
|
|
183
157
|
}
|
|
184
158
|
|
|
185
159
|
/**
|
|
186
160
|
* 分割复合命令为管道段
|
|
161
|
+
* v1.1 修复: 正确处理引号内的 |
|
|
187
162
|
*/
|
|
188
163
|
function splitPipeSegments(command) {
|
|
189
|
-
|
|
190
|
-
|
|
164
|
+
const segments = []
|
|
165
|
+
let current = ''
|
|
166
|
+
let inSingle = false
|
|
167
|
+
let inDouble = false
|
|
168
|
+
let escaped = false
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < command.length; i++) {
|
|
171
|
+
const ch = command[i]
|
|
172
|
+
|
|
173
|
+
if (escaped) {
|
|
174
|
+
current += ch
|
|
175
|
+
escaped = false
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (ch === '\\') {
|
|
180
|
+
escaped = true
|
|
181
|
+
current += ch
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (ch === "'" && !inDouble) {
|
|
186
|
+
inSingle = !inSingle
|
|
187
|
+
current += ch
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (ch === '"' && !inSingle) {
|
|
192
|
+
inDouble = !inDouble
|
|
193
|
+
current += ch
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (ch === '|' && !inSingle && !inDouble) {
|
|
198
|
+
// 检查是否是 || (逻辑或)
|
|
199
|
+
if (command[i + 1] === '|') {
|
|
200
|
+
current += '||'
|
|
201
|
+
i++ // 跳过下一个 |
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
segments.push(current.trim())
|
|
205
|
+
current = ''
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
current += ch
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (current.trim()) {
|
|
213
|
+
segments.push(current.trim())
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return segments.filter(s => s.length > 0)
|
|
191
217
|
}
|
|
192
218
|
|
|
193
219
|
/**
|
|
@@ -209,11 +235,20 @@ export function checkBashSafety(command) {
|
|
|
209
235
|
}
|
|
210
236
|
}
|
|
211
237
|
|
|
212
|
-
// 2.
|
|
213
|
-
const
|
|
238
|
+
// 2. 命令替换注入检测(v1.1 新增)
|
|
239
|
+
const substitutionFindings = checkCommandSubstitution(command)
|
|
240
|
+
for (const finding of substitutionFindings) {
|
|
241
|
+
if (finding.blocked) {
|
|
242
|
+
reasons.push(`[${finding.severity.toUpperCase()}] ${finding.reason}`)
|
|
243
|
+
if (finding.severity === 'critical' || (finding.severity === 'high' && maxSeverity !== 'critical')) {
|
|
244
|
+
maxSeverity = finding.severity
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
214
248
|
|
|
249
|
+
// 3. 管道段分析
|
|
250
|
+
const segments = splitPipeSegments(command)
|
|
215
251
|
if (segments.length > 1) {
|
|
216
|
-
// 跨段 cd+git 检查
|
|
217
252
|
const cdGit = checkCrossSegmentCdGit(segments)
|
|
218
253
|
if (cdGit.blocked) {
|
|
219
254
|
reasons.push(`[HIGH] ${cdGit.reason}`)
|
|
@@ -221,23 +256,24 @@ export function checkBashSafety(command) {
|
|
|
221
256
|
}
|
|
222
257
|
}
|
|
223
258
|
|
|
224
|
-
//
|
|
259
|
+
// 4. 多 cd 检测
|
|
225
260
|
const multiCd = checkMultipleCd(segments)
|
|
226
261
|
if (multiCd.blocked) {
|
|
227
262
|
reasons.push(`[MEDIUM] ${multiCd.reason}`)
|
|
228
263
|
if (maxSeverity === 'none') maxSeverity = 'medium'
|
|
229
264
|
}
|
|
230
265
|
|
|
231
|
-
//
|
|
266
|
+
// 5. 网络外泄检查 — curl/wget 到私有 IP
|
|
232
267
|
const netMatch = command.match(/\b(curl|wget)\s+.*?(https?:\/\/[^\s&|;]+)/g)
|
|
233
268
|
if (netMatch) {
|
|
234
269
|
for (const match of netMatch) {
|
|
235
270
|
const urlMatch = match.match(/(https?:\/\/[^\s&|;]+)/)
|
|
236
271
|
if (urlMatch) {
|
|
237
272
|
try {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
273
|
+
const parsed = new URL(urlMatch[1])
|
|
274
|
+
const hostname = parsed.hostname
|
|
275
|
+
// 完整的内网 IP 检查(含 127.x.x.x)
|
|
276
|
+
if (/^(0\.|10\.|127\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/.test(hostname)) {
|
|
241
277
|
reasons.push(`[CRITICAL] curl/wget 到内网地址 ${hostname},疑似数据外泄`)
|
|
242
278
|
maxSeverity = 'critical'
|
|
243
279
|
}
|
|
@@ -246,8 +282,8 @@ export function checkBashSafety(command) {
|
|
|
246
282
|
}
|
|
247
283
|
}
|
|
248
284
|
|
|
249
|
-
//
|
|
250
|
-
if (/>>?\s*\/(etc|boot|usr)\//.test(command)) {
|
|
285
|
+
// 6. 重定向到敏感位置
|
|
286
|
+
if (/>>?\s*\/(etc|boot|usr|proc|sys)\//.test(command)) {
|
|
251
287
|
reasons.push('[CRITICAL] 输出重定向到系统目录')
|
|
252
288
|
maxSeverity = 'critical'
|
|
253
289
|
}
|
|
@@ -266,14 +302,11 @@ export function formatSafetyReport(result) {
|
|
|
266
302
|
if (result.allowed && result.reasons.length === 0) {
|
|
267
303
|
return '✅ 命令安全检查通过'
|
|
268
304
|
}
|
|
269
|
-
|
|
270
305
|
const lines = result.allowed
|
|
271
306
|
? ['⚠️ 命令安全检查发现注意事项:']
|
|
272
307
|
: ['🚫 命令被安全策略阻止:']
|
|
273
|
-
|
|
274
308
|
for (const reason of result.reasons) {
|
|
275
309
|
lines.push(` ${reason}`)
|
|
276
310
|
}
|
|
277
|
-
|
|
278
311
|
return lines.join('\n')
|
|
279
312
|
}
|