koishi-plugin-spawn-modified 1.2.7 → 1.2.8

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/utils.js ADDED
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildRegex = buildRegex;
7
+ exports.isCommandBlocked = isCommandBlocked;
8
+ exports.stripQuotes = stripQuotes;
9
+ exports.tokenizeCommand = tokenizeCommand;
10
+ exports.isPathLike = isPathLike;
11
+ exports.resolveCandidatePath = resolveCandidatePath;
12
+ exports.extractPathCandidates = extractPathCandidates;
13
+ exports.isWithinRoot = isWithinRoot;
14
+ exports.validatePathAccess = validatePathAccess;
15
+ exports.validateCdCommand = validateCdCommand;
16
+ exports.maskCurlOutput = maskCurlOutput;
17
+ exports.isPrivateIpv4 = isPrivateIpv4;
18
+ exports.escapeHtml = escapeHtml;
19
+ var os_1 = __importDefault(require("os"));
20
+ var path_1 = __importDefault(require("path"));
21
+ // 命令过滤:支持黑名单/白名单模式
22
+ function buildRegex(entry) {
23
+ try {
24
+ return new RegExp(entry, 'i');
25
+ }
26
+ catch (_) {
27
+ // 回退为逐字匹配,防止用户写了非法正则
28
+ var escaped = entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29
+ try {
30
+ return new RegExp(escaped, 'i');
31
+ }
32
+ catch (_) {
33
+ return null;
34
+ }
35
+ }
36
+ }
37
+ function isCommandBlocked(command, mode, list) {
38
+ if (!(list === null || list === void 0 ? void 0 : list.length))
39
+ return false;
40
+ var trimmedCommand = command.trim();
41
+ var hit = list.some(function (entry) {
42
+ var regex = buildRegex(entry);
43
+ return regex ? regex.test(trimmedCommand) : false;
44
+ });
45
+ return mode === 'blacklist' ? hit : !hit;
46
+ }
47
+ function stripQuotes(text) {
48
+ return text.replace(/^['"]|['"]$/g, '');
49
+ }
50
+ function tokenizeCommand(command) {
51
+ var tokens = [];
52
+ var current = '';
53
+ var quote = null;
54
+ for (var i = 0; i < command.length; i++) {
55
+ var char = command[i];
56
+ if ((char === '"' || char === "'") && (quote === null || quote === char)) {
57
+ quote = quote ? null : char;
58
+ continue;
59
+ }
60
+ if (!quote && /\s/.test(char)) {
61
+ if (current) {
62
+ tokens.push(current);
63
+ current = '';
64
+ }
65
+ continue;
66
+ }
67
+ current += char;
68
+ }
69
+ if (current)
70
+ tokens.push(current);
71
+ return tokens;
72
+ }
73
+ function isPathLike(token) {
74
+ var trimmed = token.trim();
75
+ if (!trimmed)
76
+ return false;
77
+ if (/^[|&><]+$/.test(trimmed))
78
+ return false;
79
+ if (/^-{1,2}[a-zA-Z0-9][\w-]*$/.test(trimmed))
80
+ return false;
81
+ if (/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed))
82
+ return false;
83
+ var normalized = stripQuotes(trimmed);
84
+ return (/^[A-Za-z]:[\\\/]/.test(normalized) ||
85
+ normalized.startsWith('/') ||
86
+ normalized.startsWith('~') ||
87
+ normalized.startsWith('..') ||
88
+ normalized.startsWith('./') ||
89
+ normalized.includes('/') ||
90
+ normalized.includes('\\'));
91
+ }
92
+ function resolveCandidatePath(candidate, currentDir) {
93
+ var _a;
94
+ var cleaned = stripQuotes(candidate.trim());
95
+ var homeDir = ((_a = os_1.default.homedir) === null || _a === void 0 ? void 0 : _a.call(os_1.default)) || '';
96
+ if (cleaned.startsWith('~')) {
97
+ var withoutTilde = cleaned.slice(1).replace(/^[/\\]/, '');
98
+ var homeResolved = homeDir ? path_1.default.join(homeDir, withoutTilde) : cleaned;
99
+ return path_1.default.resolve(homeResolved);
100
+ }
101
+ return path_1.default.resolve(currentDir, cleaned);
102
+ }
103
+ function extractPathCandidates(command) {
104
+ var tokens = tokenizeCommand(command);
105
+ var candidates = [];
106
+ for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) {
107
+ var token = tokens_1[_i];
108
+ var normalized = stripQuotes(token);
109
+ if (isPathLike(normalized)) {
110
+ candidates.push(normalized);
111
+ continue;
112
+ }
113
+ var eqIndex = normalized.indexOf('=');
114
+ if (eqIndex > 0) {
115
+ var value = normalized.slice(eqIndex + 1);
116
+ if (isPathLike(value)) {
117
+ candidates.push(value);
118
+ }
119
+ }
120
+ }
121
+ return candidates;
122
+ }
123
+ // 解析 cd 命令并验证路径
124
+ function isWithinRoot(rootDir, targetPath) {
125
+ var relative = path_1.default.relative(rootDir, targetPath);
126
+ return relative === '' || (!relative.startsWith('..') && !path_1.default.isAbsolute(relative));
127
+ }
128
+ function validatePathAccess(command, currentDir, rootDir, restrictDirectory) {
129
+ if (!restrictDirectory)
130
+ return { valid: true };
131
+ var normalizedRoot = path_1.default.resolve(rootDir);
132
+ var candidates = extractPathCandidates(command);
133
+ for (var _i = 0, candidates_1 = candidates; _i < candidates_1.length; _i++) {
134
+ var candidate = candidates_1[_i];
135
+ var resolved = resolveCandidatePath(candidate, currentDir);
136
+ if (!isWithinRoot(normalizedRoot, resolved)) {
137
+ return { valid: false, error: 'restricted-path' };
138
+ }
139
+ }
140
+ return { valid: true };
141
+ }
142
+ function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
143
+ if (!restrictDirectory)
144
+ return { valid: true };
145
+ var normalizedRoot = path_1.default.resolve(rootDir);
146
+ var cdMatches = [];
147
+ var cdRegex = /\bcd\s+([^;&|\n]+)/gi;
148
+ var m;
149
+ while ((m = cdRegex.exec(command)) !== null) {
150
+ cdMatches.push(m);
151
+ }
152
+ if (!cdMatches.length)
153
+ return { valid: true };
154
+ // 若命令被链式运算符分隔且包含 cd,则要求所有 cd 目标都在指定 root 下,否则拒绝
155
+ for (var _i = 0, cdMatches_1 = cdMatches; _i < cdMatches_1.length; _i++) {
156
+ var match = cdMatches_1[_i];
157
+ var target = match[1].trim().replace(/['"]/g, '');
158
+ var absolutePath = path_1.default.resolve(currentDir, target);
159
+ if (!isWithinRoot(normalizedRoot, absolutePath)) {
160
+ return { valid: false, error: 'restricted-directory' };
161
+ }
162
+ }
163
+ // 仅当命令是单独的 cd 时才更新会话目录,避免链式命令切换目录后执行其他操作
164
+ var singleCdOnly = /^\s*cd\s+[^;&|\n]+\s*$/i.test(command);
165
+ if (singleCdOnly) {
166
+ var target = cdMatches[0][1].trim().replace(/['"]/g, '');
167
+ var absolutePath = path_1.default.resolve(currentDir, target);
168
+ return { valid: true, newDir: absolutePath };
169
+ }
170
+ return { valid: true };
171
+ }
172
+ function maskCurlOutput(command, output) {
173
+ if (!output)
174
+ return output;
175
+ if (!/\bcurl\b/i.test(command))
176
+ return output;
177
+ var ipv4Regex = /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g;
178
+ return output.replace(ipv4Regex, function (ip) { return (isPrivateIpv4(ip) ? ip : '*.*.*.*'); });
179
+ }
180
+ function isPrivateIpv4(ip) {
181
+ var octets = ip.split('.').map(Number);
182
+ if (octets.length !== 4)
183
+ return false;
184
+ if (octets.some(function (octet) { return Number.isNaN(octet) || octet < 0 || octet > 255; }))
185
+ return false;
186
+ var a = octets[0], b = octets[1];
187
+ if (a === 10)
188
+ return true;
189
+ if (a === 172 && b >= 16 && b <= 31)
190
+ return true;
191
+ if (a === 192 && b === 168)
192
+ return true;
193
+ if (a === 127)
194
+ return true;
195
+ if (a === 169 && b === 254)
196
+ return true;
197
+ return false;
198
+ }
199
+ function escapeHtml(text) {
200
+ return text
201
+ .replace(/&/g, '&amp;')
202
+ .replace(/</g, '&lt;')
203
+ .replace(/>/g, '&gt;')
204
+ .replace(/"/g, '&quot;')
205
+ .replace(/'/g, '&#039;');
206
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-spawn-modified",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "description": "Run shell commands with Koishi",
5
5
  "keywords": [
6
6
  "bot",
package/src/config.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { Schema, Time } from 'koishi'
2
+
3
+ const encodings = ['utf8', 'utf16le', 'latin1', 'ucs2'] as const
4
+
5
+ export interface Config {
6
+ root?: string
7
+ shell?: string
8
+ encoding?: typeof encodings[number]
9
+ timeout?: number
10
+ debug?: boolean
11
+ renderImage?: boolean
12
+ exemptUsers?: string[]
13
+ blockedCommands?: string[]
14
+ restrictDirectory?: boolean
15
+ authority?: number
16
+ commandFilterMode?: 'blacklist' | 'whitelist'
17
+ commandList?: string[]
18
+ }
19
+
20
+ export const Config: Schema<Config> = Schema.object({
21
+ root: Schema.string().description('工作路径。').default(''),
22
+ shell: Schema.string().description('运行命令的程序。'),
23
+ encoding: Schema.union(encodings).description('输出内容编码。').default('utf8'),
24
+ timeout: Schema.number().description('最长运行时间。').default(Time.minute),
25
+ debug: Schema.boolean().description('开启调试模式,将群组ID、用户ID等信息输出到日志。').default(false),
26
+ renderImage: Schema.boolean().description('是否将命令执行结果渲染为图片(需要安装 puppeteer 插件)。').default(false),
27
+ exemptUsers: Schema.array(String).description('例外用户列表,格式为 "群组ID:用户ID"。私聊时群组ID为0。匹配的用户将无视一切过滤器。').default([]),
28
+ blockedCommands: Schema.array(String).description('违禁命令列表(命令的开头部分)。').default([]),
29
+ restrictDirectory: Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
30
+ authority: Schema.number().description('exec 命令所需权限等级。').default(4),
31
+ commandFilterMode: Schema.union(['blacklist', 'whitelist']).description('命令过滤模式:blacklist/whitelist').default('blacklist'),
32
+ commandList: Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([]),
33
+ })