@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,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 权限系统增强版 — 规则持久化 + 审计日志
|
|
3
|
-
*
|
|
3
|
+
* 对应原版:src/hooks/toolPermission/ + src/utils/permissions/
|
|
4
4
|
*/
|
|
5
|
-
import { readFile, writeFile, mkdir } from 'fs/promises'
|
|
6
|
-
import {
|
|
5
|
+
import { readFile, writeFile, mkdir, stat, rename } from 'fs/promises'
|
|
6
|
+
import { join } from 'path'
|
|
7
7
|
import { checkBashSafety } from './bash-guard.js'
|
|
8
8
|
import { checkPathSafety, checkWritePathSafety } from './path-guard.js'
|
|
9
9
|
import { checkUrlSafety } from './ssrf-guard.js'
|
|
10
10
|
|
|
11
11
|
const PERMISSIONS_FILE = '.claude-code/permissions.json'
|
|
12
12
|
const AUDIT_LOG_FILE = '.claude-code/audit.log'
|
|
13
|
+
const AUDIT_LOG_MAX_SIZE = 10 * 1024 * 1024 // 10MB 审计日志上限
|
|
14
|
+
const AUDIT_LOG_MAX_BACKUPS = 3 // 最多保留 3 个轮转备份
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* 权限决策类型
|
|
@@ -25,10 +27,10 @@ export const PermissionDecision = {
|
|
|
25
27
|
*/
|
|
26
28
|
export class PermissionRule {
|
|
27
29
|
constructor({ tool, pattern, decision, reason, expiresAt = null }) {
|
|
28
|
-
this.tool = tool
|
|
29
|
-
this.pattern = pattern
|
|
30
|
-
this.decision = decision
|
|
31
|
-
this.reason = reason
|
|
30
|
+
this.tool = tool // 工具名或 '*'(所有工具)
|
|
31
|
+
this.pattern = pattern // 匹配模式(glob 或 regex 字符串)
|
|
32
|
+
this.decision = decision // allow / deny / ask
|
|
33
|
+
this.reason = reason // 规则原因
|
|
32
34
|
this.createdAt = Date.now()
|
|
33
35
|
this.expiresAt = expiresAt // 过期时间(会话级规则)
|
|
34
36
|
}
|
|
@@ -41,21 +43,17 @@ export class PermissionRule {
|
|
|
41
43
|
/** 检查输入是否匹配此规则 */
|
|
42
44
|
matches(input) {
|
|
43
45
|
if (this.isExpired) return false
|
|
44
|
-
|
|
45
46
|
// 简单 glob 匹配
|
|
46
47
|
const pattern = this.pattern
|
|
47
48
|
if (pattern === '*') return true
|
|
48
|
-
|
|
49
49
|
// 路径模式
|
|
50
50
|
if (typeof input === 'string' && input.includes('/')) {
|
|
51
51
|
return this._globMatch(pattern, input)
|
|
52
52
|
}
|
|
53
|
-
|
|
54
53
|
// 命令模式
|
|
55
54
|
if (typeof input === 'string') {
|
|
56
55
|
return input.startsWith(pattern) || this._globMatch(pattern, input)
|
|
57
56
|
}
|
|
58
|
-
|
|
59
57
|
return false
|
|
60
58
|
}
|
|
61
59
|
|
|
@@ -74,8 +72,8 @@ export class PermissionRule {
|
|
|
74
72
|
export class EnhancedPermissionChecker {
|
|
75
73
|
constructor(mode = 'ask', options = {}) {
|
|
76
74
|
this.mode = mode
|
|
77
|
-
this.rules = []
|
|
78
|
-
this.auditLog = []
|
|
75
|
+
this.rules = [] // PermissionRule 列表
|
|
76
|
+
this.auditLog = [] // 审计日志
|
|
79
77
|
this.cwd = options.cwd || process.cwd()
|
|
80
78
|
this.projectDir = options.projectDir || process.cwd()
|
|
81
79
|
this._maxAuditEntries = 1000
|
|
@@ -117,7 +115,7 @@ export class EnhancedPermissionChecker {
|
|
|
117
115
|
* 综合权限检查
|
|
118
116
|
* @param {string} toolName — 工具名
|
|
119
117
|
* @param {object} input — 工具输入参数
|
|
120
|
-
* @returns {Promise<{allowed: boolean, reason?: string, securityCheck?: object}>}
|
|
118
|
+
* @returns {Promise<{allowed: boolean, reason?: string, requiresConfirmation?: boolean, securityCheck?: object}>}
|
|
121
119
|
*/
|
|
122
120
|
async check(toolName, input = {}) {
|
|
123
121
|
// 1. 模式级检查
|
|
@@ -125,6 +123,7 @@ export class EnhancedPermissionChecker {
|
|
|
125
123
|
this._log(toolName, input, false, '全局拒绝模式')
|
|
126
124
|
return { allowed: false, reason: '全局拒绝模式' }
|
|
127
125
|
}
|
|
126
|
+
|
|
128
127
|
if (this.mode === 'always-allow') {
|
|
129
128
|
const securityResult = await this._securityCheck(toolName, input)
|
|
130
129
|
if (!securityResult.safe) {
|
|
@@ -141,7 +140,7 @@ export class EnhancedPermissionChecker {
|
|
|
141
140
|
if (rule.tool === toolName || rule.tool === '*') {
|
|
142
141
|
if (rule.matches(this._extractPattern(toolName, input))) {
|
|
143
142
|
if (rule.decision === PermissionDecision.DENY) {
|
|
144
|
-
this._log(toolName, input, false,
|
|
143
|
+
this._log(toolName, input, false, `规则拒绝:${rule.reason}`)
|
|
145
144
|
return { allowed: false, reason: rule.reason }
|
|
146
145
|
}
|
|
147
146
|
if (rule.decision === PermissionDecision.ALLOW) {
|
|
@@ -151,7 +150,7 @@ export class EnhancedPermissionChecker {
|
|
|
151
150
|
this._log(toolName, input, false, securityResult.reason)
|
|
152
151
|
return { allowed: false, reason: securityResult.reason, securityCheck: securityResult }
|
|
153
152
|
}
|
|
154
|
-
this._log(toolName, input, true,
|
|
153
|
+
this._log(toolName, input, true, `规则允许:${rule.reason}`)
|
|
155
154
|
return { allowed: true }
|
|
156
155
|
}
|
|
157
156
|
}
|
|
@@ -166,9 +165,12 @@ export class EnhancedPermissionChecker {
|
|
|
166
165
|
}
|
|
167
166
|
|
|
168
167
|
// 4. ask 模式 — 需要用户确认
|
|
168
|
+
// 返回 requiresConfirmation=true,让调用方处理确认逻辑
|
|
169
169
|
this._log(toolName, input, true, 'ask 模式 — 等待用户确认')
|
|
170
170
|
return {
|
|
171
|
-
allowed:
|
|
171
|
+
allowed: false, // ask 模式下先拒绝,等待用户确认
|
|
172
|
+
requiresConfirmation: true, // 标记需要用户确认
|
|
173
|
+
reason: 'ask 模式需要用户确认',
|
|
172
174
|
securityCheck: securityResult,
|
|
173
175
|
}
|
|
174
176
|
}
|
|
@@ -188,7 +190,6 @@ export class EnhancedPermissionChecker {
|
|
|
188
190
|
detail: result,
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
|
-
|
|
192
193
|
case 'Read': {
|
|
193
194
|
const filePath = input.file_path || ''
|
|
194
195
|
if (!filePath) return { safe: true }
|
|
@@ -199,7 +200,6 @@ export class EnhancedPermissionChecker {
|
|
|
199
200
|
detail: result,
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
|
-
|
|
203
203
|
case 'Edit':
|
|
204
204
|
case 'Write': {
|
|
205
205
|
const filePath = input.file_path || ''
|
|
@@ -212,7 +212,6 @@ export class EnhancedPermissionChecker {
|
|
|
212
212
|
detail: result,
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
|
-
|
|
216
215
|
case 'WebFetch': {
|
|
217
216
|
const url = input.url || ''
|
|
218
217
|
if (!url) return { safe: true }
|
|
@@ -222,7 +221,6 @@ export class EnhancedPermissionChecker {
|
|
|
222
221
|
reason: result.reason,
|
|
223
222
|
}
|
|
224
223
|
}
|
|
225
|
-
|
|
226
224
|
default:
|
|
227
225
|
return { safe: true }
|
|
228
226
|
}
|
|
@@ -231,15 +229,20 @@ export class EnhancedPermissionChecker {
|
|
|
231
229
|
/** 提取规则匹配用的模式字符串 */
|
|
232
230
|
_extractPattern(toolName, input) {
|
|
233
231
|
switch (toolName) {
|
|
234
|
-
case 'Bash':
|
|
232
|
+
case 'Bash':
|
|
233
|
+
return input.command || ''
|
|
235
234
|
case 'Read':
|
|
236
235
|
case 'Edit':
|
|
237
|
-
case 'Write':
|
|
236
|
+
case 'Write':
|
|
237
|
+
return input.file_path || ''
|
|
238
238
|
case 'Glob':
|
|
239
|
-
case 'Grep':
|
|
239
|
+
case 'Grep':
|
|
240
|
+
return input.path || input.pattern || ''
|
|
240
241
|
case 'WebFetch':
|
|
241
|
-
case 'WebSearch':
|
|
242
|
-
|
|
242
|
+
case 'WebSearch':
|
|
243
|
+
return input.url || input.query || ''
|
|
244
|
+
default:
|
|
245
|
+
return JSON.stringify(input)
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
248
|
|
|
@@ -252,7 +255,6 @@ export class EnhancedPermissionChecker {
|
|
|
252
255
|
allowed,
|
|
253
256
|
reason,
|
|
254
257
|
})
|
|
255
|
-
|
|
256
258
|
// 限制审计日志大小
|
|
257
259
|
if (this.auditLog.length > this._maxAuditEntries) {
|
|
258
260
|
this.auditLog = this.auditLog.slice(-this._maxAuditEntries)
|
|
@@ -271,7 +273,11 @@ export class EnhancedPermissionChecker {
|
|
|
271
273
|
decision: r.decision,
|
|
272
274
|
reason: r.reason,
|
|
273
275
|
}))
|
|
274
|
-
|
|
276
|
+
// v1.1 修复:文件权限 0600(仅所有者可读写),防止其他用户读取权限规则
|
|
277
|
+
await writeFile(join(dir, 'permissions.json'), JSON.stringify(data, null, 2), {
|
|
278
|
+
encoding: 'utf-8',
|
|
279
|
+
mode: 0o600,
|
|
280
|
+
})
|
|
275
281
|
}
|
|
276
282
|
|
|
277
283
|
/** 加载权限规则 */
|
|
@@ -287,19 +293,51 @@ export class EnhancedPermissionChecker {
|
|
|
287
293
|
}
|
|
288
294
|
}
|
|
289
295
|
|
|
290
|
-
/**
|
|
296
|
+
/** 保存审计日志(v1.1: 增加轮转,防止磁盘耗尽) */
|
|
291
297
|
async saveAuditLog() {
|
|
292
298
|
const dir = join(this.projectDir, '.claude-code')
|
|
293
299
|
await mkdir(dir, { recursive: true })
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
300
|
+
const logPath = join(dir, 'audit.log')
|
|
301
|
+
|
|
302
|
+
// 检查现有日志大小,超过上限则轮转
|
|
303
|
+
try {
|
|
304
|
+
const logStat = await stat(logPath)
|
|
305
|
+
if (logStat.size >= AUDIT_LOG_MAX_SIZE) {
|
|
306
|
+
// 轮转:audit.log → audit.log.1 → audit.log.2 → audit.log.3(最老的删除)
|
|
307
|
+
for (let i = AUDIT_LOG_MAX_BACKUPS; i >= 1; i--) {
|
|
308
|
+
const src = i === 1 ? logPath : join(dir, `audit.log.${i - 1}`)
|
|
309
|
+
const dst = join(dir, `audit.log.${i}`)
|
|
310
|
+
try {
|
|
311
|
+
if (i === AUDIT_LOG_MAX_BACKUPS) {
|
|
312
|
+
// 最老的备份直接删除
|
|
313
|
+
const { unlink } = await import('fs/promises')
|
|
314
|
+
await unlink(dst).catch(() => {})
|
|
315
|
+
}
|
|
316
|
+
await rename(src, dst).catch(() => {})
|
|
317
|
+
} catch {
|
|
318
|
+
/* 忽略轮转错误 */
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
/* 日志文件不存在,首次写入 */
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const lines = this.auditLog
|
|
327
|
+
.map(e => `${e.timestamp} | ${e.tool} | ${e.allowed ? 'ALLOW' : 'DENY'} | ${e.reason} | ${e.inputSnippet}`)
|
|
328
|
+
.join('\n')
|
|
329
|
+
// v1.1 修复:文件权限 0600
|
|
330
|
+
await writeFile(logPath, lines, { encoding: 'utf-8', mode: 0o600 })
|
|
298
331
|
}
|
|
299
332
|
|
|
300
333
|
/** 获取审计摘要 */
|
|
301
334
|
getAuditSummary() {
|
|
302
|
-
const summary = {
|
|
335
|
+
const summary = {
|
|
336
|
+
total: this.auditLog.length,
|
|
337
|
+
allowed: 0,
|
|
338
|
+
denied: 0,
|
|
339
|
+
byTool: {},
|
|
340
|
+
}
|
|
303
341
|
for (const entry of this.auditLog) {
|
|
304
342
|
if (entry.allowed) summary.allowed++
|
|
305
343
|
else summary.denied++
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 路径安全防护 — 防止路径遍历攻击和敏感文件访问
|
|
3
3
|
* 对应原版: src/utils/permissions/filesystem.ts + 多处路径检查
|
|
4
|
+
*
|
|
5
|
+
* v1.1 修复:
|
|
6
|
+
* - /proc/self/ 加入禁止路径(容器逃逸/内存泄露)
|
|
7
|
+
* - 新增 URL 编码绕过检测(%2e%2e, %2f, %5c 等)
|
|
8
|
+
* - 双重编码绕过检测(%252e 等)
|
|
4
9
|
*/
|
|
5
10
|
import { resolve, normalize, isAbsolute, relative, sep } from 'path'
|
|
11
|
+
import { realpathSync } from 'fs'
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* 敏感路径列表 — 禁止读写
|
|
@@ -16,6 +22,7 @@ const FORBIDDEN_PATHS = [
|
|
|
16
22
|
'/etc/pam.d/',
|
|
17
23
|
'/boot/',
|
|
18
24
|
'/proc/sys/',
|
|
25
|
+
'/proc/self/', // v1.1: 阻止 /proc/self 访问(容器逃逸/内存泄露)
|
|
19
26
|
'/sys/kernel/',
|
|
20
27
|
]
|
|
21
28
|
|
|
@@ -31,15 +38,32 @@ const SENSITIVE_PREFIXES = [
|
|
|
31
38
|
|
|
32
39
|
/**
|
|
33
40
|
* 规范化路径 — 解析 .., ., 符号链接等
|
|
41
|
+
* v1.1 修复: 增加编码绕过检测(%2e%2e, %252e 等 URL 编码变形)
|
|
34
42
|
* @param {string} filePath — 输入路径
|
|
35
43
|
* @param {string} cwd — 当前工作目录
|
|
36
44
|
* @returns {string} 规范化后的绝对路径
|
|
37
45
|
*/
|
|
38
46
|
export function sanitizePath(filePath, cwd = process.cwd()) {
|
|
47
|
+
// v1.1: 解码 URL 编码绕过(%2e = ., %2f = /, %5c = \)
|
|
48
|
+
let decoded = filePath
|
|
49
|
+
// 双重编码先解
|
|
50
|
+
decoded = decoded.replace(/%252e/gi, '.').replace(/%252f/gi, '/').replace(/%255c/gi, '\\')
|
|
51
|
+
// 单次编码
|
|
52
|
+
decoded = decoded.replace(/%2e/gi, '.').replace(/%2f/gi, '/').replace(/%5c/gi, '\\')
|
|
53
|
+
|
|
39
54
|
// 如果是相对路径,基于 cwd 解析
|
|
40
|
-
const absPath = isAbsolute(
|
|
55
|
+
const absPath = isAbsolute(decoded) ? decoded : resolve(cwd, decoded)
|
|
41
56
|
// 规范化:消除 .. 和 .
|
|
42
|
-
|
|
57
|
+
const normalized = normalize(absPath)
|
|
58
|
+
|
|
59
|
+
// M6 fix: 解析符号链接,防止通过 symlink 绕过路径安全检查
|
|
60
|
+
// 例如: /tmp/link → /etc/shadow
|
|
61
|
+
try {
|
|
62
|
+
return realpathSync(normalized)
|
|
63
|
+
} catch {
|
|
64
|
+
// 文件不存在时 realpathSync 会抛错,返回规范化路径即可
|
|
65
|
+
return normalized
|
|
66
|
+
}
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
/**
|
|
@@ -52,7 +76,16 @@ export function sanitizePath(filePath, cwd = process.cwd()) {
|
|
|
52
76
|
export function checkPathTraversal(filePath, cwd = process.cwd(), allowedDirs = []) {
|
|
53
77
|
const resolvedPath = sanitizePath(filePath, cwd)
|
|
54
78
|
|
|
55
|
-
// 1.
|
|
79
|
+
// 1. v1.1: 检查原始输入中的编码绕过尝试
|
|
80
|
+
if (/%2e|%2f|%5c|%252e|%252f/i.test(filePath)) {
|
|
81
|
+
return {
|
|
82
|
+
safe: false,
|
|
83
|
+
resolvedPath,
|
|
84
|
+
reason: `路径编码绕过检测: ${filePath} 包含 URL 编码字符,疑似路径遍历攻击`,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. 检查 .. 在原始路径中的使用
|
|
56
89
|
if (filePath.includes('..')) {
|
|
57
90
|
const normalizedRelative = relative(cwd, resolvedPath)
|
|
58
91
|
if (normalizedRelative.startsWith('..') || resolvedPath.startsWith('/etc/') || resolvedPath.startsWith('/root/')) {
|
|
@@ -64,13 +97,12 @@ export function checkPathTraversal(filePath, cwd = process.cwd(), allowedDirs =
|
|
|
64
97
|
}
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
//
|
|
100
|
+
// 3. 检查是否在允许的目录范围内
|
|
68
101
|
if (allowedDirs.length > 0) {
|
|
69
102
|
const isInAllowedDir = allowedDirs.some(dir => {
|
|
70
103
|
const normDir = normalize(isAbsolute(dir) ? dir : resolve(cwd, dir))
|
|
71
104
|
return resolvedPath.startsWith(normDir + sep) || resolvedPath === normDir
|
|
72
105
|
})
|
|
73
|
-
|
|
74
106
|
if (!isInAllowedDir) {
|
|
75
107
|
return {
|
|
76
108
|
safe: false,
|
|
@@ -79,7 +111,6 @@ export function checkPathTraversal(filePath, cwd = process.cwd(), allowedDirs =
|
|
|
79
111
|
}
|
|
80
112
|
}
|
|
81
113
|
}
|
|
82
|
-
|
|
83
114
|
return { safe: true, resolvedPath }
|
|
84
115
|
}
|
|
85
116
|
|
|
@@ -92,41 +123,26 @@ export function checkForbiddenPath(resolvedPath) {
|
|
|
92
123
|
// 1. 严格匹配禁止路径
|
|
93
124
|
for (const forbidden of FORBIDDEN_PATHS) {
|
|
94
125
|
if (resolvedPath === forbidden || resolvedPath.startsWith(forbidden + sep) || resolvedPath.startsWith(forbidden + '/')) {
|
|
95
|
-
return {
|
|
96
|
-
allowed: false,
|
|
97
|
-
reason: `禁止访问敏感路径: ${resolvedPath}(匹配规则: ${forbidden})`,
|
|
98
|
-
}
|
|
126
|
+
return { allowed: false, reason: `禁止访问敏感路径: ${resolvedPath}(匹配规则: ${forbidden})` }
|
|
99
127
|
}
|
|
100
128
|
}
|
|
101
129
|
|
|
102
130
|
// 2. SSH 目录特殊处理
|
|
103
131
|
if (resolvedPath.includes('/.ssh/') || resolvedPath.includes('\\.ssh\\')) {
|
|
104
|
-
// 允许读取 known_hosts 和 config,禁止读取私钥
|
|
105
132
|
const sshKeyPattern = /\/\.ssh\/id_(rsa|ed25519|ecdsa|dsa)(\.pub)?$/i
|
|
106
133
|
const sshConfigPattern = /\/\.ssh\/(config|known_hosts|authorized_keys)$/i
|
|
107
|
-
|
|
108
134
|
if (sshKeyPattern.test(resolvedPath)) {
|
|
109
|
-
return {
|
|
110
|
-
allowed: false,
|
|
111
|
-
reason: `禁止访问 SSH 密钥文件: ${resolvedPath}`,
|
|
112
|
-
}
|
|
135
|
+
return { allowed: false, reason: `禁止访问 SSH 密钥文件: ${resolvedPath}` }
|
|
113
136
|
}
|
|
114
|
-
|
|
115
137
|
if (sshConfigPattern.test(resolvedPath)) {
|
|
116
|
-
return {
|
|
117
|
-
allowed: true,
|
|
118
|
-
reason: `⚠️ 访问 SSH 配置文件: ${resolvedPath}`,
|
|
119
|
-
}
|
|
138
|
+
return { allowed: true, reason: `⚠️ 访问 SSH 配置文件: ${resolvedPath}` }
|
|
120
139
|
}
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
// 3. 敏感前缀检查 — 允许但提示
|
|
124
143
|
for (const prefix of SENSITIVE_PREFIXES) {
|
|
125
144
|
if (resolvedPath.startsWith(prefix)) {
|
|
126
|
-
return {
|
|
127
|
-
allowed: true,
|
|
128
|
-
reason: `⚠️ 访问系统敏感目录: ${resolvedPath}`,
|
|
129
|
-
}
|
|
145
|
+
return { allowed: true, reason: `⚠️ 访问系统敏感目录: ${resolvedPath}` }
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
|
|
@@ -143,7 +159,6 @@ export function checkPathSafety(filePath, options = {}) {
|
|
|
143
159
|
const { cwd = process.cwd(), allowedDirs = [], checkForbidden = true } = options
|
|
144
160
|
const reasons = []
|
|
145
161
|
|
|
146
|
-
// 路径遍历检查
|
|
147
162
|
const traversalResult = checkPathTraversal(filePath, cwd, allowedDirs)
|
|
148
163
|
const resolvedPath = traversalResult.resolvedPath
|
|
149
164
|
|
|
@@ -151,7 +166,6 @@ export function checkPathSafety(filePath, options = {}) {
|
|
|
151
166
|
reasons.push(traversalResult.reason)
|
|
152
167
|
}
|
|
153
168
|
|
|
154
|
-
// 禁止路径检查
|
|
155
169
|
if (checkForbidden) {
|
|
156
170
|
const forbiddenResult = checkForbiddenPath(resolvedPath)
|
|
157
171
|
if (!forbiddenResult.allowed) {
|
|
@@ -162,7 +176,7 @@ export function checkPathSafety(filePath, options = {}) {
|
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
return {
|
|
165
|
-
safe: !reasons.some(r => r.startsWith('禁止') || r.startsWith('路径遍历')),
|
|
179
|
+
safe: !reasons.some(r => r.startsWith('禁止') || r.startsWith('路径遍历') || r.startsWith('路径编码绕过')),
|
|
166
180
|
resolvedPath,
|
|
167
181
|
reasons,
|
|
168
182
|
}
|
|
@@ -178,13 +192,12 @@ export function checkWritePathSafety(filePath, options = {}) {
|
|
|
178
192
|
const result = checkPathSafety(filePath, options)
|
|
179
193
|
|
|
180
194
|
// 写入额外检查:不能写到系统关键目录
|
|
181
|
-
const systemWriteDirs = ['/etc/', '/boot/', '/usr/bin/', '/usr/lib/', '/sbin/', '/bin/']
|
|
195
|
+
const systemWriteDirs = ['/etc/', '/boot/', '/usr/bin/', '/usr/lib/', '/sbin/', '/bin/', '/proc/', '/sys/']
|
|
182
196
|
for (const dir of systemWriteDirs) {
|
|
183
197
|
if (result.resolvedPath.startsWith(dir)) {
|
|
184
198
|
result.safe = false
|
|
185
199
|
result.reasons.push(`禁止写入系统关键目录: ${dir}`)
|
|
186
200
|
}
|
|
187
201
|
}
|
|
188
|
-
|
|
189
202
|
return result
|
|
190
203
|
}
|