koishi-plugin-spawn-modified 1.2.3 → 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.
Files changed (3) hide show
  1. package/lib/index.js +45 -10
  2. package/package.json +1 -1
  3. package/src/index.ts +50 -14
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 = {
@@ -101,20 +117,39 @@ function isCommandBlocked(command, mode, list) {
101
117
  return mode === 'blacklist' ? hit : !hit;
102
118
  }
103
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
+ }
104
124
  function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
105
- var cdMatch = command.trim().match(/^cd\s+(.+)$/i);
106
- if (!cdMatch)
107
- return { valid: true };
108
125
  if (!restrictDirectory)
109
126
  return { valid: true };
110
- var targetPath = cdMatch[1].trim().replace(/['"]/g, '');
111
- var absolutePath = path_1.default.resolve(currentDir, targetPath);
112
127
  var normalizedRoot = path_1.default.resolve(rootDir);
113
- // 检查目标路径是否在根目录内
114
- if (!absolutePath.startsWith(normalizedRoot)) {
115
- return { valid: false, error: 'restricted-directory' };
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 };
116
151
  }
117
- return { valid: true, newDir: absolutePath };
152
+ return { valid: true };
118
153
  }
119
154
  // 渲染终端输出为图片
120
155
  function renderTerminalImage(ctx, workingDir, command, output) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-spawn-modified",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Run shell commands with Koishi",
5
5
  "keywords": [
6
6
  "bot",
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 {
@@ -84,22 +100,42 @@ function isCommandBlocked(command: string, mode: 'blacklist' | 'whitelist', list
84
100
  }
85
101
 
86
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
+
87
108
  function validateCdCommand(command: string, currentDir: string, rootDir: string, restrictDirectory: boolean): { valid: boolean; newDir?: string; error?: string } {
88
- const cdMatch = command.trim().match(/^cd\s+(.+)$/i)
89
- if (!cdMatch) return { valid: true }
90
-
91
109
  if (!restrictDirectory) return { valid: true }
92
-
93
- const targetPath = cdMatch[1].trim().replace(/['"]/g, '')
94
- const absolutePath = path.resolve(currentDir, targetPath)
110
+
95
111
  const normalizedRoot = path.resolve(rootDir)
96
-
97
- // 检查目标路径是否在根目录内
98
- if (!absolutePath.startsWith(normalizedRoot)) {
99
- return { valid: false, error: 'restricted-directory' }
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)
100
117
  }
101
-
102
- return { valid: true, newDir: absolutePath }
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 }
103
139
  }
104
140
 
105
141
  // 渲染终端输出为图片
@@ -292,7 +328,7 @@ export function apply(ctx: Context, config: Config) {
292
328
  }
293
329
 
294
330
  command = h('', h.parse(command)).toString(true)
295
- // 检查命令过滤(黑/白名单)
331
+ // 检查命令过滤(黑/白名单);仅使用配置提供的正则
296
332
  const filterList = (config.commandList?.length ? config.commandList : config.blockedCommands) || []
297
333
  const filterMode = config.commandFilterMode || 'blacklist'
298
334
  if (isCommandBlocked(command, filterMode, filterList)) {