@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/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: { ...process.env, ...env },
134
+ env: sanitizeEnv(env),
37
135
  })
38
136
 
39
137
  this.process.stdout.on('data', (data) => {
@@ -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, join } from 'path'
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
- * 1. 危险命令模式检测(rm -rf /、dd、mkfs 等)
7
- * 2. cd + git 组合攻击(裸仓库 fsmonitor 绕过)
8
- * 3. 管道注入(跨段 cd+git)
9
- * 4. 反引号/命令替换注入
10
- * 5. 网络数据外泄(curl/wget 到可疑地址)
11
- * 6. 敏感文件访问(/etc/shadow、SSH 密钥)
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
- pattern: /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?\/\s*$/,
23
- reason: '递归删除根目录',
24
- severity: 'critical',
25
- },
26
- {
27
- pattern: /\brm\s+.*-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*.*\s\/[a-zA-Z]*/,
28
- reason: '递归强制删除系统目录',
29
- severity: 'critical',
30
- },
31
- {
32
- pattern: /\bdd\s+if=.*of=\/dev\//,
33
- reason: 'dd 写入设备文件',
34
- severity: 'critical',
35
- },
36
- {
37
- pattern: /\bmkfs\b/,
38
- reason: '格式化文件系统',
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
- pattern: /\/etc\/(shadow|passwd|sudoers|ssh\/sshd_config)\b/,
65
- reason: '访问敏感系统文件',
66
- severity: 'high',
67
- },
68
- {
69
- pattern: /~\/\.ssh\/(id_[a-z]+|authorized_keys|config)\b/,
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
- pattern: /\bcurl\b.*\|\s*(bash|sh|zsh|fish)\b/,
82
- reason: '从网络下载并执行脚本(curl | bash)',
83
- severity: 'critical',
84
- },
85
- {
86
- pattern: /\bwget\b.*\|\s*(bash|sh|zsh|fish)\b/,
87
- reason: '从网络下载并执行脚本(wget | sh)',
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
- pattern: /\bsudo\s+su\b/,
104
- reason: '切换到 root 用户',
105
- severity: 'high',
106
- },
107
- {
108
- pattern: /\bsudo\s+chmod\s+[0-7]{3,4}\s+\/(etc|usr|boot)\b/,
109
- reason: 'sudo 修改系统目录权限',
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
- pattern: /\bnsenter\b.*--target\s+1\b/,
121
- reason: '容器 namespace 逃逸到 PID 1',
122
- severity: 'critical',
123
- },
124
- {
125
- pattern: /\/proc\/sys\/kernel\/core_pattern/,
126
- reason: '修改 core_pattern(容器逃逸技术)',
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 命令检测 — 一个命令中多次 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
- return command.split(/\s*\|\s*/).filter(s => s.trim())
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 segments = splitPipeSegments(command)
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
- // 3. 多 cd 检测
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
- // 4. 网络外泄检查 — curl/wget 到私有 IP
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 hostname = new URL(urlMatch[1]).hostname
239
- // 简单的内网域名检查
240
- if (/^(10\.|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)) {
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
- // 5. 重定向到敏感位置
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
  }