koishi-plugin-spawn-modified 1.2.2 → 1.2.4
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/lib/index.js +65 -12
- package/package.json +1 -1
- package/src/index.ts +69 -16
package/lib/index.js
CHANGED
|
@@ -66,7 +66,23 @@ exports.Config = koishi_1.Schema.object({
|
|
|
66
66
|
restrictDirectory: koishi_1.Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
|
|
67
67
|
authority: koishi_1.Schema.number().description('exec 命令所需权限等级。').default(4),
|
|
68
68
|
commandFilterMode: koishi_1.Schema.union(['blacklist', 'whitelist']).description('命令过滤模式:blacklist/whitelist').default('blacklist'),
|
|
69
|
-
commandList: koishi_1.Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([
|
|
69
|
+
commandList: koishi_1.Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([
|
|
70
|
+
'^(?:^|[;&|\\n])\\s*(?:\\.?\\.\\/[^;&|\\s]+\\.sh\\b|(?:sh|bash|zsh|ksh|dash)\\s+[^;&|\\s]+\\.sh\\b)',
|
|
71
|
+
'^(?:^|[;&|\\n])\\s*chmod\\s+\\+x\\b',
|
|
72
|
+
'^\\s*sudo\\s+rm\\s+-rf\\b',
|
|
73
|
+
'^\\s*rm\\s+-rf\\b',
|
|
74
|
+
'^\\s*rm\\s+-rf\\s+/',
|
|
75
|
+
'^\\s*rm\\s+-rf\\s+/\\*',
|
|
76
|
+
'^\\s*mkfs(\\.\\w+)?\\b',
|
|
77
|
+
'^\\s*dd\\b.*\\bof=\\/dev\\/(sd|nvme|mmcblk)',
|
|
78
|
+
'^\\s*wipefs\\b',
|
|
79
|
+
'^\\s*(parted|fdisk|cfdisk)\\b',
|
|
80
|
+
'^\\s*lvremove\\b|^\\s*vgremove\\b',
|
|
81
|
+
'^\\s*cryptsetup\\b.*(erase|format)',
|
|
82
|
+
'^\\s*shutdown\\b|^\\s*poweroff\\b|^\\s*reboot\\b',
|
|
83
|
+
'^\\s*chmod\\s+[-+]?(777|666)\\b',
|
|
84
|
+
'^\\s*echo\\s+.+\\s*>\\s*/etc/(passwd|shadow|sudoers)\\b',
|
|
85
|
+
]),
|
|
70
86
|
});
|
|
71
87
|
exports.name = 'spawn';
|
|
72
88
|
exports.inject = {
|
|
@@ -75,28 +91,65 @@ exports.inject = {
|
|
|
75
91
|
// 当前工作目录状态管理
|
|
76
92
|
var sessionDirs = new Map();
|
|
77
93
|
// 命令过滤:支持黑名单/白名单模式
|
|
94
|
+
function buildRegex(entry) {
|
|
95
|
+
try {
|
|
96
|
+
return new RegExp(entry, 'i');
|
|
97
|
+
}
|
|
98
|
+
catch (_) {
|
|
99
|
+
// 回退为逐字匹配,防止用户写了非法正则
|
|
100
|
+
var escaped = entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
101
|
+
try {
|
|
102
|
+
return new RegExp(escaped, 'i');
|
|
103
|
+
}
|
|
104
|
+
catch (_) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
78
109
|
function isCommandBlocked(command, mode, list) {
|
|
79
110
|
if (!(list === null || list === void 0 ? void 0 : list.length))
|
|
80
111
|
return false;
|
|
81
|
-
var trimmedCommand = command.trim()
|
|
82
|
-
var hit = list.some(function (entry) {
|
|
112
|
+
var trimmedCommand = command.trim();
|
|
113
|
+
var hit = list.some(function (entry) {
|
|
114
|
+
var regex = buildRegex(entry);
|
|
115
|
+
return regex ? regex.test(trimmedCommand) : false;
|
|
116
|
+
});
|
|
83
117
|
return mode === 'blacklist' ? hit : !hit;
|
|
84
118
|
}
|
|
85
119
|
// 解析 cd 命令并验证路径
|
|
120
|
+
function isWithinRoot(rootDir, targetPath) {
|
|
121
|
+
var relative = path_1.default.relative(rootDir, targetPath);
|
|
122
|
+
return relative === '' || (!relative.startsWith('..') && !path_1.default.isAbsolute(relative));
|
|
123
|
+
}
|
|
86
124
|
function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
|
|
87
|
-
var cdMatch = command.trim().match(/^cd\s+(.+)$/i);
|
|
88
|
-
if (!cdMatch)
|
|
89
|
-
return { valid: true };
|
|
90
125
|
if (!restrictDirectory)
|
|
91
126
|
return { valid: true };
|
|
92
|
-
var targetPath = cdMatch[1].trim().replace(/['"]/g, '');
|
|
93
|
-
var absolutePath = path_1.default.resolve(currentDir, targetPath);
|
|
94
127
|
var normalizedRoot = path_1.default.resolve(rootDir);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
128
|
+
var cdMatches = [];
|
|
129
|
+
var cdRegex = /\bcd\s+([^;&|\n]+)/gi;
|
|
130
|
+
var m;
|
|
131
|
+
while ((m = cdRegex.exec(command)) !== null) {
|
|
132
|
+
cdMatches.push(m);
|
|
133
|
+
}
|
|
134
|
+
if (!cdMatches.length)
|
|
135
|
+
return { valid: true };
|
|
136
|
+
// 若命令被链式运算符分隔且包含 cd,则要求所有 cd 目标都在指定 root 下,否则拒绝
|
|
137
|
+
for (var _i = 0, cdMatches_1 = cdMatches; _i < cdMatches_1.length; _i++) {
|
|
138
|
+
var match = cdMatches_1[_i];
|
|
139
|
+
var target = match[1].trim().replace(/['"]/g, '');
|
|
140
|
+
var absolutePath = path_1.default.resolve(currentDir, target);
|
|
141
|
+
if (!isWithinRoot(normalizedRoot, absolutePath)) {
|
|
142
|
+
return { valid: false, error: 'restricted-directory' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 仅当命令是单独的 cd 时才更新会话目录,避免链式命令切换目录后执行其他操作
|
|
146
|
+
var singleCdOnly = /^\s*cd\s+[^;&|\n]+\s*$/i.test(command);
|
|
147
|
+
if (singleCdOnly) {
|
|
148
|
+
var target = cdMatches[0][1].trim().replace(/['"]/g, '');
|
|
149
|
+
var absolutePath = path_1.default.resolve(currentDir, target);
|
|
150
|
+
return { valid: true, newDir: absolutePath };
|
|
98
151
|
}
|
|
99
|
-
return { valid: true
|
|
152
|
+
return { valid: true };
|
|
100
153
|
}
|
|
101
154
|
// 渲染终端输出为图片
|
|
102
155
|
function renderTerminalImage(ctx, workingDir, command, output) {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -37,7 +37,23 @@ export const Config: Schema<Config> = Schema.object({
|
|
|
37
37
|
restrictDirectory: Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
|
|
38
38
|
authority: Schema.number().description('exec 命令所需权限等级。').default(4),
|
|
39
39
|
commandFilterMode: Schema.union(['blacklist', 'whitelist']).description('命令过滤模式:blacklist/whitelist').default('blacklist'),
|
|
40
|
-
commandList: Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([
|
|
40
|
+
commandList: Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([
|
|
41
|
+
'^(?:^|[;&|\\n])\\s*(?:\\.?\\.\\/[^;&|\\s]+\\.sh\\b|(?:sh|bash|zsh|ksh|dash)\\s+[^;&|\\s]+\\.sh\\b)',
|
|
42
|
+
'^(?:^|[;&|\\n])\\s*chmod\\s+\\+x\\b',
|
|
43
|
+
'^\\s*sudo\\s+rm\\s+-rf\\b',
|
|
44
|
+
'^\\s*rm\\s+-rf\\b',
|
|
45
|
+
'^\\s*rm\\s+-rf\\s+/',
|
|
46
|
+
'^\\s*rm\\s+-rf\\s+/\\*',
|
|
47
|
+
'^\\s*mkfs(\\.\\w+)?\\b',
|
|
48
|
+
'^\\s*dd\\b.*\\bof=\\/dev\\/(sd|nvme|mmcblk)',
|
|
49
|
+
'^\\s*wipefs\\b',
|
|
50
|
+
'^\\s*(parted|fdisk|cfdisk)\\b',
|
|
51
|
+
'^\\s*lvremove\\b|^\\s*vgremove\\b',
|
|
52
|
+
'^\\s*cryptsetup\\b.*(erase|format)',
|
|
53
|
+
'^\\s*shutdown\\b|^\\s*poweroff\\b|^\\s*reboot\\b',
|
|
54
|
+
'^\\s*chmod\\s+[-+]?(777|666)\\b',
|
|
55
|
+
'^\\s*echo\\s+.+\\s*>\\s*/etc/(passwd|shadow|sudoers)\\b',
|
|
56
|
+
]),
|
|
41
57
|
})
|
|
42
58
|
|
|
43
59
|
export interface State {
|
|
@@ -59,30 +75,67 @@ export const inject = {
|
|
|
59
75
|
const sessionDirs = new Map<string, string>()
|
|
60
76
|
|
|
61
77
|
// 命令过滤:支持黑名单/白名单模式
|
|
78
|
+
function buildRegex(entry: string): RegExp | null {
|
|
79
|
+
try {
|
|
80
|
+
return new RegExp(entry, 'i')
|
|
81
|
+
} catch (_) {
|
|
82
|
+
// 回退为逐字匹配,防止用户写了非法正则
|
|
83
|
+
const escaped = entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
84
|
+
try {
|
|
85
|
+
return new RegExp(escaped, 'i')
|
|
86
|
+
} catch (_) {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
62
92
|
function isCommandBlocked(command: string, mode: 'blacklist' | 'whitelist', list: string[]): boolean {
|
|
63
93
|
if (!list?.length) return false
|
|
64
|
-
const trimmedCommand = command.trim()
|
|
65
|
-
const hit = list.some(entry =>
|
|
94
|
+
const trimmedCommand = command.trim()
|
|
95
|
+
const hit = list.some(entry => {
|
|
96
|
+
const regex = buildRegex(entry)
|
|
97
|
+
return regex ? regex.test(trimmedCommand) : false
|
|
98
|
+
})
|
|
66
99
|
return mode === 'blacklist' ? hit : !hit
|
|
67
100
|
}
|
|
68
101
|
|
|
69
102
|
// 解析 cd 命令并验证路径
|
|
103
|
+
function isWithinRoot(rootDir: string, targetPath: string): boolean {
|
|
104
|
+
const relative = path.relative(rootDir, targetPath)
|
|
105
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
106
|
+
}
|
|
107
|
+
|
|
70
108
|
function validateCdCommand(command: string, currentDir: string, rootDir: string, restrictDirectory: boolean): { valid: boolean; newDir?: string; error?: string } {
|
|
71
|
-
const cdMatch = command.trim().match(/^cd\s+(.+)$/i)
|
|
72
|
-
if (!cdMatch) return { valid: true }
|
|
73
|
-
|
|
74
109
|
if (!restrictDirectory) return { valid: true }
|
|
75
|
-
|
|
76
|
-
const targetPath = cdMatch[1].trim().replace(/['"]/g, '')
|
|
77
|
-
const absolutePath = path.resolve(currentDir, targetPath)
|
|
110
|
+
|
|
78
111
|
const normalizedRoot = path.resolve(rootDir)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
const cdMatches: RegExpExecArray[] = []
|
|
113
|
+
const cdRegex = /\bcd\s+([^;&|\n]+)/gi
|
|
114
|
+
let m: RegExpExecArray | null
|
|
115
|
+
while ((m = cdRegex.exec(command)) !== null) {
|
|
116
|
+
cdMatches.push(m)
|
|
83
117
|
}
|
|
84
|
-
|
|
85
|
-
return { valid: true
|
|
118
|
+
|
|
119
|
+
if (!cdMatches.length) return { valid: true }
|
|
120
|
+
|
|
121
|
+
// 若命令被链式运算符分隔且包含 cd,则要求所有 cd 目标都在指定 root 下,否则拒绝
|
|
122
|
+
for (const match of cdMatches) {
|
|
123
|
+
const target = match[1].trim().replace(/['"]/g, '')
|
|
124
|
+
const absolutePath = path.resolve(currentDir, target)
|
|
125
|
+
if (!isWithinRoot(normalizedRoot, absolutePath)) {
|
|
126
|
+
return { valid: false, error: 'restricted-directory' }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 仅当命令是单独的 cd 时才更新会话目录,避免链式命令切换目录后执行其他操作
|
|
131
|
+
const singleCdOnly = /^\s*cd\s+[^;&|\n]+\s*$/i.test(command)
|
|
132
|
+
if (singleCdOnly) {
|
|
133
|
+
const target = cdMatches[0][1].trim().replace(/['"]/g, '')
|
|
134
|
+
const absolutePath = path.resolve(currentDir, target)
|
|
135
|
+
return { valid: true, newDir: absolutePath }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { valid: true }
|
|
86
139
|
}
|
|
87
140
|
|
|
88
141
|
// 渲染终端输出为图片
|
|
@@ -275,7 +328,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
275
328
|
}
|
|
276
329
|
|
|
277
330
|
command = h('', h.parse(command)).toString(true)
|
|
278
|
-
//
|
|
331
|
+
// 检查命令过滤(黑/白名单);仅使用配置提供的正则
|
|
279
332
|
const filterList = (config.commandList?.length ? config.commandList : config.blockedCommands) || []
|
|
280
333
|
const filterMode = config.commandFilterMode || 'blacklist'
|
|
281
334
|
if (isCommandBlocked(command, filterMode, filterList)) {
|