zen-gitsync 2.11.38 → 2.12.2
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 +695 -667
- package/package.json +1 -1
- package/src/ui/public/assets/EditorView-CHBjgiZc.css +1 -0
- package/src/ui/public/assets/EditorView-bnJmBq-i.js +21 -0
- package/src/ui/public/assets/SourceMapView-DhQX0K7t.css +1 -0
- package/src/ui/public/assets/SourceMapView-Rz5SD0A0.js +3 -0
- package/src/ui/public/assets/index-Bo3tntQh.js +73 -0
- package/src/ui/public/assets/{index-JF9fRS1G.css → index-bOs5P8fz.css} +1 -1
- package/src/ui/public/assets/{vendor-ITcl-z5O.js → vendor-DITsiaGj.js} +290 -290
- package/src/ui/public/assets/{vendor-BdheoY37.css → vendor-q83wvJns.css} +1 -1
- package/src/ui/public/index.html +4 -4
- package/src/ui/server/.claude/codediff.txt +6 -0
- package/src/ui/server/routes/fs.js +33 -45
- package/src/ui/server/utils/pathGuard.js +141 -0
- package/src/ui/server/utils/pathGuard.test.js +124 -0
- package/src/utils/index.js +1044 -1044
- package/src/ui/public/assets/devopicons-QN4QXivI.woff2 +0 -0
- package/src/ui/public/assets/file-icons-C0jOugUK.woff2 +0 -0
- package/src/ui/public/assets/fontawesome-B-jkhYfk.woff2 +0 -0
- package/src/ui/public/assets/index-BxVtrDYE.js +0 -95
- package/src/ui/public/assets/mfixx-CpAhKOZz.woff2 +0 -0
- package/src/ui/public/assets/octicons-CaZ_fok2.woff2 +0 -0
|
@@ -5,6 +5,7 @@ import path from 'path';
|
|
|
5
5
|
import open from 'open';
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import { spawn, exec } from 'child_process';
|
|
8
|
+
import { ensureWithinCwd } from '../utils/pathGuard.js';
|
|
8
9
|
|
|
9
10
|
export function registerFsRoutes({
|
|
10
11
|
app,
|
|
@@ -17,6 +18,12 @@ export function registerFsRoutes({
|
|
|
17
18
|
setProjectRoomId,
|
|
18
19
|
setIsGitRepo
|
|
19
20
|
}) {
|
|
21
|
+
// ── 解析并校验 user 输入路径在当前项目 cwd 内(防 ../ 父目录逃逸、startsWith 假阳性、Windows 大小写)──
|
|
22
|
+
const safePathInProject = async (userPath) => {
|
|
23
|
+
const cwd = getCurrentProjectPath() || process.cwd()
|
|
24
|
+
return ensureWithinCwd(userPath, cwd)
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
// 新增获取当前工作目录接口
|
|
21
28
|
app.get('/api/current_directory', async (req, res) => {
|
|
22
29
|
try {
|
|
@@ -497,12 +504,9 @@ export function registerFsRoutes({
|
|
|
497
504
|
try {
|
|
498
505
|
const filePath = req.query.path;
|
|
499
506
|
if (!filePath) return res.status(400).json({ success: false, error: '缺少 path 参数' });
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const resolved =
|
|
503
|
-
if (!resolved.startsWith(path.resolve(cwd))) {
|
|
504
|
-
return res.status(403).json({ success: false, error: '禁止访问工作目录以外的文件' });
|
|
505
|
-
}
|
|
507
|
+
const safe = await safePathInProject(filePath);
|
|
508
|
+
if (!safe) return res.status(403).json({ success: false, error: '禁止访问工作目录以外的文件' });
|
|
509
|
+
const resolved = safe.safePath;
|
|
506
510
|
const stat = await fs.stat(resolved);
|
|
507
511
|
if (!stat.isFile()) return res.status(400).json({ success: false, error: '目标不是文件' });
|
|
508
512
|
// 超过 2MB 不读取
|
|
@@ -521,11 +525,9 @@ export function registerFsRoutes({
|
|
|
521
525
|
try {
|
|
522
526
|
const filePath = req.query.path;
|
|
523
527
|
if (!filePath) return res.status(400).end();
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return res.status(403).end();
|
|
528
|
-
}
|
|
528
|
+
const safe = await safePathInProject(filePath);
|
|
529
|
+
if (!safe) return res.status(403).end();
|
|
530
|
+
const resolved = safe.safePath;
|
|
529
531
|
const stat = await fs.stat(resolved);
|
|
530
532
|
if (!stat.isFile()) return res.status(400).end();
|
|
531
533
|
if (stat.size > 20 * 1024 * 1024) return res.status(413).end();
|
|
@@ -552,12 +554,9 @@ export function registerFsRoutes({
|
|
|
552
554
|
if (!filePath || content === undefined) {
|
|
553
555
|
return res.status(400).json({ success: false, error: '缺少 path 或 content 参数' });
|
|
554
556
|
}
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return res.status(403).json({ success: false, error: '禁止写入工作目录以外的文件' });
|
|
559
|
-
}
|
|
560
|
-
await fs.writeFile(resolved, content, 'utf-8');
|
|
557
|
+
const safe = await safePathInProject(filePath);
|
|
558
|
+
if (!safe) return res.status(403).json({ success: false, error: '禁止写入工作目录以外的文件' });
|
|
559
|
+
await fs.writeFile(safe.safePath, content, 'utf-8');
|
|
561
560
|
res.json({ success: true });
|
|
562
561
|
} catch (error) {
|
|
563
562
|
res.status(500).json({ success: false, error: error.message });
|
|
@@ -569,11 +568,9 @@ export function registerFsRoutes({
|
|
|
569
568
|
try {
|
|
570
569
|
const { path: filePath } = req.body;
|
|
571
570
|
if (!filePath) return res.status(400).json({ success: false, error: '缺少 path 参数' });
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
return res.status(403).json({ success: false, error: '禁止在工作目录以外创建文件' });
|
|
576
|
-
}
|
|
571
|
+
const safe = await safePathInProject(filePath);
|
|
572
|
+
if (!safe) return res.status(403).json({ success: false, error: '禁止在工作目录以外创建文件' });
|
|
573
|
+
const resolved = safe.safePath;
|
|
577
574
|
// 如果已存在则拒绝
|
|
578
575
|
try {
|
|
579
576
|
await fs.access(resolved);
|
|
@@ -592,11 +589,9 @@ export function registerFsRoutes({
|
|
|
592
589
|
try {
|
|
593
590
|
const { path: dirPath } = req.body;
|
|
594
591
|
if (!dirPath) return res.status(400).json({ success: false, error: '缺少 path 参数' });
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
return res.status(403).json({ success: false, error: '禁止在工作目录以外创建目录' });
|
|
599
|
-
}
|
|
592
|
+
const safe = await safePathInProject(dirPath);
|
|
593
|
+
if (!safe) return res.status(403).json({ success: false, error: '禁止在工作目录以外创建目录' });
|
|
594
|
+
const resolved = safe.safePath;
|
|
600
595
|
try {
|
|
601
596
|
await fs.access(resolved);
|
|
602
597
|
return res.status(409).json({ success: false, error: '目录已存在' });
|
|
@@ -613,11 +608,9 @@ export function registerFsRoutes({
|
|
|
613
608
|
try {
|
|
614
609
|
const filePath = req.query.path;
|
|
615
610
|
if (!filePath) return res.status(400).json({ success: false, error: '缺少 path 参数' });
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
return res.status(403).json({ success: false, error: '禁止删除工作目录以外的内容' });
|
|
620
|
-
}
|
|
611
|
+
const safe = await safePathInProject(filePath);
|
|
612
|
+
if (!safe) return res.status(403).json({ success: false, error: '禁止删除工作目录以外的内容' });
|
|
613
|
+
const resolved = safe.safePath;
|
|
621
614
|
const stat = await fs.stat(resolved);
|
|
622
615
|
if (stat.isDirectory()) {
|
|
623
616
|
await fs.rm(resolved, { recursive: true, force: true });
|
|
@@ -635,17 +628,14 @@ export function registerFsRoutes({
|
|
|
635
628
|
try {
|
|
636
629
|
const { oldPath, newPath } = req.body;
|
|
637
630
|
if (!oldPath || !newPath) return res.status(400).json({ success: false, error: '缺少参数' });
|
|
638
|
-
const
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
if (!resolvedOld.startsWith(path.resolve(cwd)) || !resolvedNew.startsWith(path.resolve(cwd))) {
|
|
642
|
-
return res.status(403).json({ success: false, error: '禁止操作工作目录以外的内容' });
|
|
643
|
-
}
|
|
631
|
+
const safeOld = await safePathInProject(oldPath)
|
|
632
|
+
const safeNew = await safePathInProject(newPath)
|
|
633
|
+
if (!safeOld || !safeNew) return res.status(403).json({ success: false, error: '禁止操作工作目录以外的内容' });
|
|
644
634
|
try {
|
|
645
|
-
await fs.access(
|
|
635
|
+
await fs.access(safeNew.safePath);
|
|
646
636
|
return res.status(409).json({ success: false, error: '目标名称已存在' });
|
|
647
637
|
} catch { /* 不存在,继续 */ }
|
|
648
|
-
await fs.rename(
|
|
638
|
+
await fs.rename(safeOld.safePath, safeNew.safePath);
|
|
649
639
|
res.json({ success: true });
|
|
650
640
|
} catch (error) {
|
|
651
641
|
res.status(500).json({ success: false, error: error.message });
|
|
@@ -661,11 +651,9 @@ export function registerFsRoutes({
|
|
|
661
651
|
const targetPath = req.body?.path;
|
|
662
652
|
if (!targetPath) return res.status(400).json({ success: false, error: '缺少 path 参数' });
|
|
663
653
|
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
return res.status(403).json({ success: false, error: '禁止访问工作目录以外的内容' });
|
|
668
|
-
}
|
|
654
|
+
const safe = await safePathInProject(targetPath);
|
|
655
|
+
if (!safe) return res.status(403).json({ success: false, error: '禁止访问工作目录以外的内容' });
|
|
656
|
+
const resolved = safe.safePath;
|
|
669
657
|
|
|
670
658
|
try {
|
|
671
659
|
await fs.access(resolved);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路径越界检查中间件 / 工具
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* // 1. 中间件模式:自动从 req.query.path 提取并校验
|
|
6
|
+
* router.get('/api/foo', pathGuard('path'), handler)
|
|
7
|
+
*
|
|
8
|
+
* // 2. 工具函数:在 handler 里手动校验
|
|
9
|
+
* const safePath = ensureWithinCwd(userPath, cwd)
|
|
10
|
+
*
|
|
11
|
+
* 防护:
|
|
12
|
+
* - 解析 `..`、相对路径
|
|
13
|
+
* - 拒绝以 `..` 开头或解析后是绝对路径的相对路径
|
|
14
|
+
* - Windows 大小写不敏感
|
|
15
|
+
* - 真实路径 (realpath) 防符号链接
|
|
16
|
+
*
|
|
17
|
+
* @typedef {Object} PathGuardOptions
|
|
18
|
+
* @property {string} field - 从 req.query / req.body 取的字段名(默认 'path')
|
|
19
|
+
* @property {boolean} [allowMissing] - 字段缺失时是否通过(默认 false)
|
|
20
|
+
* @property {boolean} [realpath] - 是否用 fs.realpath 进一步防符号链接(默认 false)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'fs/promises'
|
|
24
|
+
import path from 'path'
|
|
25
|
+
|
|
26
|
+
const isWindows = process.platform === 'win32'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 把 user 输入路径解析成"在 cwd 内的绝对路径",越界时返回 null
|
|
30
|
+
*
|
|
31
|
+
* @param {string} input - 用户提供的路径(相对 / 绝对 / 含 .. / 符号链接)
|
|
32
|
+
* @param {string} cwd - 允许的根目录(当前项目目录)
|
|
33
|
+
* @param {{ realpath?: boolean }} [opts]
|
|
34
|
+
* @returns {Promise<{ safePath: string, realPath: string | null } | null>}
|
|
35
|
+
* - null 表示越界
|
|
36
|
+
* - safePath: 用于后续 path.resolve 等操作的绝对路径
|
|
37
|
+
* - realPath: realpath 结果(如果 opts.realpath=true 且文件存在)
|
|
38
|
+
*/
|
|
39
|
+
export async function ensureWithinCwd(input, cwd, opts = {}) {
|
|
40
|
+
if (typeof input !== 'string' || !input) return null
|
|
41
|
+
if (typeof cwd !== 'string' || !cwd) return null
|
|
42
|
+
|
|
43
|
+
let resolved
|
|
44
|
+
try {
|
|
45
|
+
resolved = path.resolve(cwd, input)
|
|
46
|
+
} catch {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// path.relative 返回以 .. 开头的字符串代表越界(其它平台绝对路径返回绝对路径本身)
|
|
51
|
+
const rel = path.relative(cwd, resolved)
|
|
52
|
+
if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
|
|
53
|
+
// 在 cwd 内(相等或子路径)
|
|
54
|
+
} else {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Windows 大小写不敏感:把 cwd 和 resolved 都转小写再比一次,确保 \ 不会绕过
|
|
59
|
+
if (isWindows) {
|
|
60
|
+
const lowerCwd = cwd.toLowerCase()
|
|
61
|
+
const lowerResolved = resolved.toLowerCase()
|
|
62
|
+
if (!lowerResolved.startsWith(lowerCwd)) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let realPath = null
|
|
68
|
+
if (opts.realpath) {
|
|
69
|
+
try {
|
|
70
|
+
realPath = await fs.realpath(resolved)
|
|
71
|
+
// realpath 后再校验一次(符号链接可能指向 cwd 之外)
|
|
72
|
+
const relReal = path.relative(cwd, realPath)
|
|
73
|
+
const isOutside = relReal === '..' || relReal.startsWith('..' + path.sep) || path.isAbsolute(relReal)
|
|
74
|
+
if (isOutside) return null
|
|
75
|
+
if (isWindows && !realPath.toLowerCase().startsWith(cwd.toLowerCase())) return null
|
|
76
|
+
} catch {
|
|
77
|
+
// 文件不存在 / 无权限:保持 null,让调用者决定
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { safePath: resolved, realPath }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Express 中间件工厂
|
|
86
|
+
*
|
|
87
|
+
* @param {string} cwd - 当前项目根
|
|
88
|
+
* @param {string | string[]} [fields] - 要校验的字段名(默认 'path',支持数组多字段)
|
|
89
|
+
* @param {{ realpath?: boolean }} [opts]
|
|
90
|
+
*/
|
|
91
|
+
export function pathGuard(cwd, fields = 'path', opts = {}) {
|
|
92
|
+
const fieldList = Array.isArray(fields) ? fields : [fields]
|
|
93
|
+
return async (req, res, next) => {
|
|
94
|
+
try {
|
|
95
|
+
for (const field of fieldList) {
|
|
96
|
+
const raw = req.query?.[field] ?? req.body?.[field]
|
|
97
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
98
|
+
if (opts.allowMissing) continue
|
|
99
|
+
return res.status(400).json({ success: false, error: `缺少 ${field} 参数` })
|
|
100
|
+
}
|
|
101
|
+
const result = await ensureWithinCwd(String(raw), cwd, opts)
|
|
102
|
+
if (!result) {
|
|
103
|
+
return res.status(403).json({ success: false, error: `禁止访问工作目录以外的文件: ${raw}` })
|
|
104
|
+
}
|
|
105
|
+
// 把校验后的安全路径挂到 res.locals,handler 拿 res.locals.safePath 用
|
|
106
|
+
res.locals.safePath = result.safePath
|
|
107
|
+
res.locals.safeRealPath = result.realPath
|
|
108
|
+
}
|
|
109
|
+
next()
|
|
110
|
+
} catch (e) {
|
|
111
|
+
res.status(500).json({ success: false, error: e.message })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 同步版本:只做路径解析校验,不做 realpath。用于不需要 fs 的纯路径场景
|
|
118
|
+
* @returns {string|null} 安全路径或 null
|
|
119
|
+
*/
|
|
120
|
+
export function ensureWithinCwdSync(input, cwd) {
|
|
121
|
+
if (typeof input !== 'string' || !input) return null
|
|
122
|
+
if (typeof cwd !== 'string' || !cwd) return null
|
|
123
|
+
|
|
124
|
+
let resolved
|
|
125
|
+
try {
|
|
126
|
+
resolved = path.resolve(cwd, input)
|
|
127
|
+
} catch {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const rel = path.relative(cwd, resolved)
|
|
132
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return null
|
|
133
|
+
|
|
134
|
+
if (isWindows) {
|
|
135
|
+
const lowerCwd = cwd.toLowerCase()
|
|
136
|
+
const lowerResolved = resolved.toLowerCase()
|
|
137
|
+
if (!lowerResolved.startsWith(lowerCwd)) return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return resolved
|
|
141
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// 路径越界检查单元测试(用 node:test 内置)
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import assert from 'node:assert/strict'
|
|
4
|
+
import { ensureWithinCwd, ensureWithinCwdSync, pathGuard } from '../utils/pathGuard.js'
|
|
5
|
+
|
|
6
|
+
const cwd = process.platform === 'win32' ? 'E:\\project' : '/tmp/project'
|
|
7
|
+
|
|
8
|
+
test('合法路径在 cwd 内', async () => {
|
|
9
|
+
const r = await ensureWithinCwd('src/foo.ts', cwd)
|
|
10
|
+
assert.ok(r, '应返回结果')
|
|
11
|
+
assert.match(r.safePath, /[\\/]project[\\/]src[\\/]foo\.ts$/)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('合法绝对路径在 cwd 内', async () => {
|
|
15
|
+
const absInCwd = process.platform === 'win32' ? 'E:\\project\\sub' : '/tmp/project/sub'
|
|
16
|
+
const r = await ensureWithinCwd(absInCwd, cwd)
|
|
17
|
+
assert.ok(r, '应通过')
|
|
18
|
+
assert.equal(r.safePath, absInCwd)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('../ 父目录逃逸被拒绝', async () => {
|
|
22
|
+
const r = await ensureWithinCwd('../etc/passwd', cwd)
|
|
23
|
+
assert.equal(r, null, '../ 应该被拒')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('cwd 的同级目录(startsWith 假阳性)被拒绝', async () => {
|
|
27
|
+
const evil = process.platform === 'win32' ? 'E:\\project-evil' : '/tmp/project-evil'
|
|
28
|
+
const r = await ensureWithinCwd(evil, cwd)
|
|
29
|
+
assert.equal(r, null, '同名前缀目录应该被拒')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('绝对路径在 cwd 外被拒', async () => {
|
|
33
|
+
const outside = process.platform === 'win32' ? 'C:\\Windows\\System32' : '/etc/passwd'
|
|
34
|
+
const r = await ensureWithinCwd(outside, cwd)
|
|
35
|
+
assert.equal(r, null, '绝对外部路径应该被拒')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('空字符串 / null / undefined 被拒', async () => {
|
|
39
|
+
assert.equal(await ensureWithinCwd('', cwd), null)
|
|
40
|
+
assert.equal(await ensureWithinCwd(null, cwd), null)
|
|
41
|
+
assert.equal(await ensureWithinCwd(undefined, cwd), null)
|
|
42
|
+
assert.equal(await ensureWithinCwd(123, cwd), null)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('多重 ../ 也被拒', async () => {
|
|
46
|
+
const r = await ensureWithinCwd('../../../../etc/passwd', cwd)
|
|
47
|
+
assert.equal(r, null)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('相对 cwd 自身(.)允许', async () => {
|
|
51
|
+
const r = await ensureWithinCwd('.', cwd)
|
|
52
|
+
assert.ok(r, '点 应解析为 cwd 本身')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('Windows 反斜杠与正斜杠混用允许', async () => {
|
|
56
|
+
if (process.platform !== 'win32') return
|
|
57
|
+
const r = await ensureWithinCwd('src\\sub/foo.ts', cwd)
|
|
58
|
+
assert.ok(r)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('Windows 大小写不敏感', async () => {
|
|
62
|
+
if (process.platform !== 'win32') return
|
|
63
|
+
const r = await ensureWithinCwd('SRC/FOO.TS', 'E:\\Project')
|
|
64
|
+
assert.ok(r, 'Windows 下大小写不敏感应允许')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('sync 版本对 .. 同样拒绝', () => {
|
|
68
|
+
assert.equal(ensureWithinCwdSync('../foo', cwd), null)
|
|
69
|
+
const r = ensureWithinCwdSync('sub/file', cwd)
|
|
70
|
+
assert.ok(r)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('中间件: 合法 path 通过 + 挂到 res.locals.safePath', async () => {
|
|
74
|
+
let nextCalled = false
|
|
75
|
+
const req = { query: { path: 'src/foo.ts' } }
|
|
76
|
+
const res = {
|
|
77
|
+
locals: {},
|
|
78
|
+
status() { return this },
|
|
79
|
+
json() { return this },
|
|
80
|
+
}
|
|
81
|
+
await pathGuard(cwd)(req, res, () => { nextCalled = true })
|
|
82
|
+
assert.equal(nextCalled, true)
|
|
83
|
+
assert.match(res.locals.safePath, /[\\/]src[\\/]foo\.ts$/)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('中间件: 越界 path 返回 403', async () => {
|
|
87
|
+
let nextCalled = false
|
|
88
|
+
let statusCode = 0
|
|
89
|
+
let body = null
|
|
90
|
+
const req = { query: { path: '../etc/passwd' } }
|
|
91
|
+
const res = {
|
|
92
|
+
locals: {},
|
|
93
|
+
status(c) { statusCode = c; return this },
|
|
94
|
+
json(b) { body = b; return this },
|
|
95
|
+
}
|
|
96
|
+
await pathGuard(cwd)(req, res, () => { nextCalled = true })
|
|
97
|
+
assert.equal(nextCalled, false)
|
|
98
|
+
assert.equal(statusCode, 403)
|
|
99
|
+
assert.match(body.error, /禁止访问/)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('中间件: 缺字段返回 400', async () => {
|
|
103
|
+
let statusCode = 0
|
|
104
|
+
const req = { query: {} }
|
|
105
|
+
const res = {
|
|
106
|
+
locals: {},
|
|
107
|
+
status(c) { statusCode = c; return this },
|
|
108
|
+
json() { return this },
|
|
109
|
+
}
|
|
110
|
+
await pathGuard(cwd)(req, res, () => {})
|
|
111
|
+
assert.equal(statusCode, 400)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('中间件: 多字段校验(rename 用)', async () => {
|
|
115
|
+
let nextCalled = false
|
|
116
|
+
const req = { query: { oldPath: 'src/a.ts', newPath: '../b.ts' } }
|
|
117
|
+
const res = {
|
|
118
|
+
locals: {},
|
|
119
|
+
status() { return this },
|
|
120
|
+
json() { return this },
|
|
121
|
+
}
|
|
122
|
+
await pathGuard(cwd, ['oldPath', 'newPath'])(req, res, () => { nextCalled = true })
|
|
123
|
+
assert.equal(nextCalled, false, 'newPath 越界应拒')
|
|
124
|
+
})
|