@raolin2025/claude-code-node 1.2.0 → 2.1.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/package.json +1 -1
- package/src/channel/index.js +81 -146
- package/src/channel/notify-daemon.js +454 -350
- package/src/core/cli.js +235 -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 +223 -117
- package/src/core/session.js +27 -10
- package/src/mcp/client.js +112 -4
- 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 +43 -30
- package/src/security/ssrf-guard.js +153 -50
- package/src/tools/glob.js +1 -1
- package/src/tools/web-fetch.js +3 -1
- package/src/types/index.js +2 -1
- package/src/utils/file-ops.js +2 -3
|
@@ -1,99 +1,92 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SSRF 防护 — 阻止对内网/云元数据端点的请求
|
|
3
|
-
*
|
|
3
|
+
* 对应原版:src/utils/hooks/ssrfGuard.ts
|
|
4
|
+
*
|
|
5
|
+
* v1.2 修复:
|
|
6
|
+
* - H4: 完整检测 fe80::/10 范围(fe80:: - febf::),而非仅 fe8 前缀
|
|
7
|
+
* - H3: 导出 dnsLookup 函数供 query-engine 使用自定义 DNS resolver
|
|
4
8
|
*/
|
|
5
9
|
import { lookup as dnsLookup } from 'dns'
|
|
6
10
|
import { isIP } from 'net'
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* 检查 IPv4 地址是否在应被阻止的范围内
|
|
10
|
-
*
|
|
11
|
-
* 阻止列表:
|
|
12
|
-
* - 0.0.0.0/8 — "this" 网络
|
|
13
|
-
* - 10.0.0.0/8 — 私有网络
|
|
14
|
-
* - 100.64.0.0/10 — CGNAT 共享地址(阿里云元数据 100.100.100.200)
|
|
15
|
-
* - 169.254.0.0/16 — 链路本地(AWS/GCP 云元数据)
|
|
16
|
-
* - 172.16.0.0/12 — 私有网络
|
|
17
|
-
* - 192.168.0.0/16 — 私有网络
|
|
18
|
-
* - 192.0.2.0/24 — TEST-NET-1 (RFC 5737)
|
|
19
|
-
* - 198.51.100.0/24 — TEST-NET-2
|
|
20
|
-
* - 203.0.113.0/24 — TEST-NET-3
|
|
21
|
-
*
|
|
22
|
-
* 允许:
|
|
23
|
-
* - 127.0.0.0/8 — 回环(本地开发 hook 服务器)
|
|
24
14
|
*/
|
|
25
15
|
function isBlockedV4(address) {
|
|
26
16
|
const parts = address.split('.').map(Number)
|
|
27
17
|
if (parts.length !== 4 || parts.some(n => Number.isNaN(n))) return false
|
|
18
|
+
const [a, b, c, d] = parts
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// 回环 — 允许
|
|
32
|
-
if (a === 127) return false
|
|
33
|
-
|
|
34
|
-
// 0.0.0.0/8
|
|
20
|
+
// 0.0.0.0/8 — "this" 网络
|
|
35
21
|
if (a === 0) return true
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
// 127.0.0.0/8 — 回环/localhost(SSRF 核心目标,必须阻止)
|
|
23
|
+
if (a === 127) return true
|
|
24
|
+
// 10.0.0.0/8 — 私有网络
|
|
38
25
|
if (a === 10) return true
|
|
39
|
-
|
|
40
26
|
// 100.64.0.0/10 — CGNAT (RFC 6598)
|
|
41
27
|
if (a === 100 && b >= 64 && b <= 127) return true
|
|
42
|
-
|
|
28
|
+
// 100.100.100.200 — 阿里云元数据(精确匹配)
|
|
29
|
+
if (a === 100 && b === 100 && c === 100 && d === 200) return true
|
|
43
30
|
// 169.254.0.0/16 — 链路本地,云元数据
|
|
44
31
|
if (a === 169 && b === 254) return true
|
|
45
|
-
|
|
46
32
|
// 172.16.0.0/12
|
|
47
33
|
if (a === 172 && b >= 16 && b <= 31) return true
|
|
48
|
-
|
|
49
34
|
// 192.168.0.0/16
|
|
50
35
|
if (a === 192 && b === 168) return true
|
|
51
|
-
|
|
52
36
|
// 192.0.2.0/24 — TEST-NET-1
|
|
53
|
-
if (a === 192 && b === 0 &&
|
|
54
|
-
|
|
37
|
+
if (a === 192 && b === 0 && c === 2) return true
|
|
55
38
|
// 198.51.100.0/24 — TEST-NET-2
|
|
56
|
-
if (a === 198 && b === 51 &&
|
|
57
|
-
|
|
39
|
+
if (a === 198 && b === 51 && c === 100) return true
|
|
58
40
|
// 203.0.113.0/24 — TEST-NET-3
|
|
59
|
-
if (a === 203 && b === 0 &&
|
|
41
|
+
if (a === 203 && b === 0 && c === 113) return true
|
|
60
42
|
|
|
61
43
|
return false
|
|
62
44
|
}
|
|
63
45
|
|
|
64
46
|
/**
|
|
65
47
|
* 检查 IPv6 地址是否在应被阻止的范围内
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* - :: — 未指定地址
|
|
69
|
-
* - fc00::/7 — 唯一本地地址 (ULA)
|
|
70
|
-
* - fe80::/10 — 链路本地
|
|
71
|
-
* - ::ffff:<被阻止的v4> — IPv4 映射地址
|
|
72
|
-
*
|
|
73
|
-
* 允许:
|
|
74
|
-
* - ::1 — 回环
|
|
48
|
+
*
|
|
49
|
+
* 修复 H4: 完整检测 fe80::/10 范围(fe80:: - febf::)
|
|
75
50
|
*/
|
|
76
51
|
function isBlockedV6(address) {
|
|
77
52
|
const normalized = address.toLowerCase()
|
|
78
53
|
|
|
79
|
-
// ::1 回环 —
|
|
80
|
-
if (normalized === '::1') return
|
|
81
|
-
|
|
54
|
+
// ::1 回环 — 阻止(SSRF 核心目标)
|
|
55
|
+
if (normalized === '::1') return true
|
|
82
56
|
// :: 未指定
|
|
83
57
|
if (normalized === '::' || normalized === '0:0:0:0:0:0:0:0') return true
|
|
84
58
|
|
|
85
59
|
// fc00::/7 — 唯一本地 (fc00:: - fdff::)
|
|
86
60
|
if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true
|
|
87
61
|
|
|
88
|
-
// fe80::/10 — 链路本地
|
|
89
|
-
|
|
62
|
+
// fe80::/10 — 链路本地 (fe80:: - febf::)
|
|
63
|
+
// 修复 H4: 使用数值比较而非前缀匹配
|
|
64
|
+
const firstTwoBytes = normalized.split(':')[0]
|
|
65
|
+
if (firstTwoBytes) {
|
|
66
|
+
const firstByte = parseInt(firstTwoBytes, 16)
|
|
67
|
+
if (!isNaN(firstByte) && firstByte >= 0xfe80 && firstByte <= 0xfebf) {
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
90
71
|
|
|
91
|
-
// ::ffff:<IPv4> — IPv4 映射地址
|
|
72
|
+
// ::ffff:<IPv4> — 短格式 IPv4 映射地址
|
|
92
73
|
const v4Mapped = normalized.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)
|
|
93
74
|
if (v4Mapped) {
|
|
94
75
|
return isBlockedV4(v4Mapped[1])
|
|
95
76
|
}
|
|
96
77
|
|
|
78
|
+
// 0:0:0:0:0:ffff:<IPv4> — 完整格式 IPv4 映射地址
|
|
79
|
+
const v4MappedFull = normalized.match(/^0:0:0:0:0:ffff:(\d+\.\d+\.\d+\.\d+)$/)
|
|
80
|
+
if (v4MappedFull) {
|
|
81
|
+
return isBlockedV4(v4MappedFull[1])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 64:ff9b::<IPv4> — NAT64 映射地址 (RFC 6052)
|
|
85
|
+
const nat64Match = normalized.match(/^64:ff9b::(\d+\.\d+\.\d+\.\d+)$/)
|
|
86
|
+
if (nat64Match) {
|
|
87
|
+
return isBlockedV4(nat64Match[1])
|
|
88
|
+
}
|
|
89
|
+
|
|
97
90
|
return false
|
|
98
91
|
}
|
|
99
92
|
|
|
@@ -126,6 +119,26 @@ export async function checkHostSafety(hostname) {
|
|
|
126
119
|
}
|
|
127
120
|
}
|
|
128
121
|
|
|
122
|
+
// 常见 SSRF 绕过主机名黑名单
|
|
123
|
+
const blockedHostnames = [
|
|
124
|
+
'localhost',
|
|
125
|
+
'localhost.localdomain',
|
|
126
|
+
'ip6-localhost',
|
|
127
|
+
'ip6-loopback',
|
|
128
|
+
'metadata.google.internal', // GCP 元数据
|
|
129
|
+
'metadata.internal', // AWS 元数据
|
|
130
|
+
'instance-data', // CloudStack 元数据
|
|
131
|
+
]
|
|
132
|
+
const lowerHost = hostname.toLowerCase()
|
|
133
|
+
if (blockedHostnames.includes(lowerHost)) {
|
|
134
|
+
return { allowed: false, addresses: [], reason: `主机名 ${hostname} 为已知 SSRF 目标` }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 阻止 *.internal / *.local / *.localhost 域名模式
|
|
138
|
+
if (lowerHost.endsWith('.internal') || lowerHost.endsWith('.local') || lowerHost.endsWith('.localhost')) {
|
|
139
|
+
return { allowed: false, addresses: [], reason: `主机名 ${hostname} 为内网域名` }
|
|
140
|
+
}
|
|
141
|
+
|
|
129
142
|
// DNS 解析后检查
|
|
130
143
|
return new Promise((resolve) => {
|
|
131
144
|
dnsLookup(hostname, (err, address) => {
|
|
@@ -162,10 +175,10 @@ export async function checkUrlSafety(url) {
|
|
|
162
175
|
|
|
163
176
|
// 只允许 HTTP/HTTPS
|
|
164
177
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
165
|
-
return { allowed: false, reason:
|
|
178
|
+
return { allowed: false, reason: `不支持的协议:${parsed.protocol}` }
|
|
166
179
|
}
|
|
167
180
|
|
|
168
|
-
//
|
|
181
|
+
// 检查主机名(含主机名黑名单 + DNS 解析)
|
|
169
182
|
const hostResult = await checkHostSafety(parsed.hostname)
|
|
170
183
|
if (!hostResult.allowed) {
|
|
171
184
|
return { allowed: false, reason: hostResult.reason }
|
|
@@ -176,3 +189,93 @@ export async function checkUrlSafety(url) {
|
|
|
176
189
|
return { allowed: false, reason: '无效的 URL' }
|
|
177
190
|
}
|
|
178
191
|
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 安全 DNS 解析器 — 用于防止 DNS Rebinding 攻击
|
|
195
|
+
* 返回解析后的 IP 地址列表,可直接用于 fetch 的 DNS 查找
|
|
196
|
+
* @param {string} hostname — 主机名
|
|
197
|
+
* @returns {Promise<{addresses: string[], blocked: boolean, reason?: string}>}
|
|
198
|
+
*/
|
|
199
|
+
export async function safeDnsLookup(hostname) {
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
dnsLookup(hostname, (err, address) => {
|
|
202
|
+
if (err) {
|
|
203
|
+
resolve({ addresses: [], blocked: false, reason: undefined })
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const addresses = Array.isArray(address) ? address.map(a => a.address) : [address]
|
|
208
|
+
const blockedAddrs = addresses.filter(a => isBlockedAddress(a))
|
|
209
|
+
|
|
210
|
+
if (blockedAddrs.length > 0) {
|
|
211
|
+
resolve({
|
|
212
|
+
addresses,
|
|
213
|
+
blocked: true,
|
|
214
|
+
reason: `主机 ${hostname} 解析到私有地址 ${blockedAddrs.join(', ')}`,
|
|
215
|
+
})
|
|
216
|
+
} else {
|
|
217
|
+
resolve({ addresses, blocked: false, reason: undefined })
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 创建安全 fetch 函数 — 防止 DNS Rebinding
|
|
225
|
+
* 使用预先解析的 IP 地址,避免二次 DNS 查询
|
|
226
|
+
* @param {Function} nativeFetch — 原生 fetch
|
|
227
|
+
* @returns {Function} 安全 fetch
|
|
228
|
+
*/
|
|
229
|
+
export function createSafeFetch(nativeFetch) {
|
|
230
|
+
return async function safeFetch(url, options = {}) {
|
|
231
|
+
const parsed = new URL(url)
|
|
232
|
+
|
|
233
|
+
// 只处理 HTTP/HTTPS
|
|
234
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
235
|
+
return nativeFetch(url, options)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 检查主机名安全性
|
|
239
|
+
const hostResult = await checkHostSafety(parsed.hostname)
|
|
240
|
+
if (!hostResult.allowed) {
|
|
241
|
+
throw new Error(`SSRF blocked: ${hostResult.reason}`)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 如果主机名是 IP 地址,直接使用
|
|
245
|
+
if (isIP(parsed.hostname)) {
|
|
246
|
+
return nativeFetch(url, options)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 对于域名,使用预先解析的 IP 地址
|
|
250
|
+
// 通过设置 Host header 和直接连接 IP 来防止 DNS Rebinding
|
|
251
|
+
const lookupResult = await safeDnsLookup(parsed.hostname)
|
|
252
|
+
if (lookupResult.blocked) {
|
|
253
|
+
throw new Error(`SSRF blocked: ${lookupResult.reason}`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 使用第一个非阻止的 IP 地址
|
|
257
|
+
if (lookupResult.addresses.length > 0) {
|
|
258
|
+
// 创建一个自定义 Agent 来覆盖 DNS 解析
|
|
259
|
+
// 注意:这需要 Node.js 的 http/https 模块支持
|
|
260
|
+
// 简单方案:直接修改 URL 为主机名 + IP
|
|
261
|
+
const originalHost = parsed.hostname
|
|
262
|
+
const ip = lookupResult.addresses[0]
|
|
263
|
+
|
|
264
|
+
// 创建新 URL 使用 IP 地址
|
|
265
|
+
const safeUrl = url.replace(originalHost, ip)
|
|
266
|
+
|
|
267
|
+
// 设置 Host header 为原始主机名
|
|
268
|
+
const safeOptions = {
|
|
269
|
+
...options,
|
|
270
|
+
headers: {
|
|
271
|
+
...options.headers,
|
|
272
|
+
'Host': originalHost,
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return nativeFetch(safeUrl, safeOptions)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return nativeFetch(url, options)
|
|
280
|
+
}
|
|
281
|
+
}
|
package/src/tools/glob.js
CHANGED
package/src/tools/web-fetch.js
CHANGED
|
@@ -35,6 +35,8 @@ function htmlToText(html) {
|
|
|
35
35
|
return text
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
const VERSION = '2.0.0'
|
|
39
|
+
|
|
38
40
|
export const webFetchTool = new ToolDef(
|
|
39
41
|
'WebFetch',
|
|
40
42
|
`Fetch and extract content from a URL.
|
|
@@ -69,7 +71,7 @@ Usage:
|
|
|
69
71
|
try {
|
|
70
72
|
const response = await fetch(url, {
|
|
71
73
|
headers: {
|
|
72
|
-
'User-Agent':
|
|
74
|
+
'User-Agent': `ClaudeCode-Node/${VERSION}`,
|
|
73
75
|
'Accept': 'text/html,application/json,text/plain,*/*',
|
|
74
76
|
},
|
|
75
77
|
signal: AbortSignal.timeout(30000),
|
package/src/types/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto'
|
|
1
2
|
// ====== 消息类型 ======
|
|
2
3
|
|
|
3
4
|
/** 消息角色 */
|
|
@@ -18,7 +19,7 @@ export class Message {
|
|
|
18
19
|
this.role = role
|
|
19
20
|
this.content = content
|
|
20
21
|
this.timestamp = Date.now()
|
|
21
|
-
this.id =
|
|
22
|
+
this.id = randomUUID()
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
package/src/utils/file-ops.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* 文件操作工具
|
|
3
3
|
* 对应原版: src/utils/file.ts + src/utils/fsOperations.ts
|
|
4
4
|
*/
|
|
5
|
-
import { readFile, writeFile, stat, mkdir, rm
|
|
6
|
-
import { resolve, dirname,
|
|
7
|
-
import { existsSync } from 'fs'
|
|
5
|
+
import { readFile, writeFile, stat, mkdir, rm } from 'fs/promises'
|
|
6
|
+
import { resolve, dirname, isAbsolute } from 'path'
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* 安全读取文件(带大小限制)
|