koishi-plugin-spawn-modified 1.2.4 → 1.2.6
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 +125 -19
- package/lib/locales/zh-CN.json +2 -1
- package/package.json +1 -1
- package/src/index.ts +135 -18
- package/src/locales/zh-CN.yml +1 -0
package/lib/index.js
CHANGED
|
@@ -52,6 +52,7 @@ exports.inject = exports.name = exports.Config = void 0;
|
|
|
52
52
|
exports.apply = apply;
|
|
53
53
|
var child_process_1 = require("child_process");
|
|
54
54
|
var koishi_1 = require("koishi");
|
|
55
|
+
var os_1 = __importDefault(require("os"));
|
|
55
56
|
var path_1 = __importDefault(require("path"));
|
|
56
57
|
var url_1 = require("url");
|
|
57
58
|
var ansi_to_html_1 = __importDefault(require("ansi-to-html"));
|
|
@@ -66,23 +67,7 @@ exports.Config = koishi_1.Schema.object({
|
|
|
66
67
|
restrictDirectory: koishi_1.Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
|
|
67
68
|
authority: koishi_1.Schema.number().description('exec 命令所需权限等级。').default(4),
|
|
68
69
|
commandFilterMode: koishi_1.Schema.union(['blacklist', 'whitelist']).description('命令过滤模式:blacklist/whitelist').default('blacklist'),
|
|
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
|
+
commandList: koishi_1.Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([]),
|
|
86
71
|
});
|
|
87
72
|
exports.name = 'spawn';
|
|
88
73
|
exports.inject = {
|
|
@@ -116,11 +101,101 @@ function isCommandBlocked(command, mode, list) {
|
|
|
116
101
|
});
|
|
117
102
|
return mode === 'blacklist' ? hit : !hit;
|
|
118
103
|
}
|
|
104
|
+
function stripQuotes(text) {
|
|
105
|
+
return text.replace(/^['"]|['"]$/g, '');
|
|
106
|
+
}
|
|
107
|
+
function tokenizeCommand(command) {
|
|
108
|
+
var tokens = [];
|
|
109
|
+
var current = '';
|
|
110
|
+
var quote = null;
|
|
111
|
+
for (var i = 0; i < command.length; i++) {
|
|
112
|
+
var char = command[i];
|
|
113
|
+
if ((char === '"' || char === "'") && (quote === null || quote === char)) {
|
|
114
|
+
quote = quote ? null : char;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!quote && /\s/.test(char)) {
|
|
118
|
+
if (current) {
|
|
119
|
+
tokens.push(current);
|
|
120
|
+
current = '';
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
current += char;
|
|
125
|
+
}
|
|
126
|
+
if (current)
|
|
127
|
+
tokens.push(current);
|
|
128
|
+
return tokens;
|
|
129
|
+
}
|
|
130
|
+
function isPathLike(token) {
|
|
131
|
+
var trimmed = token.trim();
|
|
132
|
+
if (!trimmed)
|
|
133
|
+
return false;
|
|
134
|
+
if (/^[|&><]+$/.test(trimmed))
|
|
135
|
+
return false;
|
|
136
|
+
if (/^-{1,2}[a-zA-Z0-9][\w-]*$/.test(trimmed))
|
|
137
|
+
return false;
|
|
138
|
+
if (/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed))
|
|
139
|
+
return false;
|
|
140
|
+
var normalized = stripQuotes(trimmed);
|
|
141
|
+
return (/^[A-Za-z]:[\\/]/.test(normalized) ||
|
|
142
|
+
normalized.startsWith('/') ||
|
|
143
|
+
normalized.startsWith('~') ||
|
|
144
|
+
normalized.startsWith('..') ||
|
|
145
|
+
normalized.startsWith('./') ||
|
|
146
|
+
normalized.includes('/') ||
|
|
147
|
+
normalized.includes('\\'));
|
|
148
|
+
}
|
|
149
|
+
function resolveCandidatePath(candidate, currentDir) {
|
|
150
|
+
var _a;
|
|
151
|
+
var cleaned = stripQuotes(candidate.trim());
|
|
152
|
+
var homeDir = ((_a = os_1.default.homedir) === null || _a === void 0 ? void 0 : _a.call(os_1.default)) || '';
|
|
153
|
+
if (cleaned.startsWith('~')) {
|
|
154
|
+
var withoutTilde = cleaned.slice(1).replace(/^[/\\]/, '');
|
|
155
|
+
var homeResolved = homeDir ? path_1.default.join(homeDir, withoutTilde) : cleaned;
|
|
156
|
+
return path_1.default.resolve(homeResolved);
|
|
157
|
+
}
|
|
158
|
+
return path_1.default.resolve(currentDir, cleaned);
|
|
159
|
+
}
|
|
160
|
+
function extractPathCandidates(command) {
|
|
161
|
+
var tokens = tokenizeCommand(command);
|
|
162
|
+
var candidates = [];
|
|
163
|
+
for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) {
|
|
164
|
+
var token = tokens_1[_i];
|
|
165
|
+
var normalized = stripQuotes(token);
|
|
166
|
+
if (isPathLike(normalized)) {
|
|
167
|
+
candidates.push(normalized);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
var eqIndex = normalized.indexOf('=');
|
|
171
|
+
if (eqIndex > 0) {
|
|
172
|
+
var value = normalized.slice(eqIndex + 1);
|
|
173
|
+
if (isPathLike(value)) {
|
|
174
|
+
candidates.push(value);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return candidates;
|
|
179
|
+
}
|
|
119
180
|
// 解析 cd 命令并验证路径
|
|
120
181
|
function isWithinRoot(rootDir, targetPath) {
|
|
121
182
|
var relative = path_1.default.relative(rootDir, targetPath);
|
|
122
183
|
return relative === '' || (!relative.startsWith('..') && !path_1.default.isAbsolute(relative));
|
|
123
184
|
}
|
|
185
|
+
function validatePathAccess(command, currentDir, rootDir, restrictDirectory) {
|
|
186
|
+
if (!restrictDirectory)
|
|
187
|
+
return { valid: true };
|
|
188
|
+
var normalizedRoot = path_1.default.resolve(rootDir);
|
|
189
|
+
var candidates = extractPathCandidates(command);
|
|
190
|
+
for (var _i = 0, candidates_1 = candidates; _i < candidates_1.length; _i++) {
|
|
191
|
+
var candidate = candidates_1[_i];
|
|
192
|
+
var resolved = resolveCandidatePath(candidate, currentDir);
|
|
193
|
+
if (!isWithinRoot(normalizedRoot, resolved)) {
|
|
194
|
+
return { valid: false, error: 'restricted-path' };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { valid: true };
|
|
198
|
+
}
|
|
124
199
|
function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
|
|
125
200
|
if (!restrictDirectory)
|
|
126
201
|
return { valid: true };
|
|
@@ -151,6 +226,33 @@ function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
|
|
|
151
226
|
}
|
|
152
227
|
return { valid: true };
|
|
153
228
|
}
|
|
229
|
+
function maskCurlOutput(command, output) {
|
|
230
|
+
if (!output)
|
|
231
|
+
return output;
|
|
232
|
+
if (!/\bcurl\b/i.test(command))
|
|
233
|
+
return output;
|
|
234
|
+
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;
|
|
235
|
+
return output.replace(ipv4Regex, function (ip) { return (isPrivateIpv4(ip) ? ip : '*.*.*.*'); });
|
|
236
|
+
}
|
|
237
|
+
function isPrivateIpv4(ip) {
|
|
238
|
+
var octets = ip.split('.').map(Number);
|
|
239
|
+
if (octets.length !== 4)
|
|
240
|
+
return false;
|
|
241
|
+
if (octets.some(function (octet) { return Number.isNaN(octet) || octet < 0 || octet > 255; }))
|
|
242
|
+
return false;
|
|
243
|
+
var a = octets[0], b = octets[1];
|
|
244
|
+
if (a === 10)
|
|
245
|
+
return true;
|
|
246
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
247
|
+
return true;
|
|
248
|
+
if (a === 192 && b === 168)
|
|
249
|
+
return true;
|
|
250
|
+
if (a === 127)
|
|
251
|
+
return true;
|
|
252
|
+
if (a === 169 && b === 254)
|
|
253
|
+
return true;
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
154
256
|
// 渲染终端输出为图片
|
|
155
257
|
function renderTerminalImage(ctx, workingDir, command, output) {
|
|
156
258
|
return __awaiter(this, void 0, void 0, function () {
|
|
@@ -226,7 +328,7 @@ function apply(ctx, config) {
|
|
|
226
328
|
ctx.i18n.define('zh-CN', require('./locales/zh-CN'));
|
|
227
329
|
ctx.command('exec <command:text>', { authority: (_a = config.authority) !== null && _a !== void 0 ? _a : 4 })
|
|
228
330
|
.action(function (_a, command_1) { return __awaiter(_this, [_a, command_1], void 0, function (_b, command) {
|
|
229
|
-
var filterList, filterMode, sessionId, rootDir, currentDir, cdValidation, timeout, state;
|
|
331
|
+
var filterList, filterMode, sessionId, rootDir, currentDir, cdValidation, pathValidation, timeout, state;
|
|
230
332
|
var _this = this;
|
|
231
333
|
var _c;
|
|
232
334
|
var session = _b.session;
|
|
@@ -249,6 +351,10 @@ function apply(ctx, config) {
|
|
|
249
351
|
if (!cdValidation.valid) {
|
|
250
352
|
return [2 /*return*/, session.text('.restricted-directory')];
|
|
251
353
|
}
|
|
354
|
+
pathValidation = validatePathAccess(command, currentDir, rootDir, config.restrictDirectory);
|
|
355
|
+
if (!pathValidation.valid) {
|
|
356
|
+
return [2 /*return*/, session.text('.restricted-path')];
|
|
357
|
+
}
|
|
252
358
|
timeout = config.timeout;
|
|
253
359
|
state = { command: command, timeout: timeout, output: '' };
|
|
254
360
|
if (!!config.renderImage) return [3 /*break*/, 2];
|
|
@@ -279,7 +385,7 @@ function apply(ctx, config) {
|
|
|
279
385
|
state.code = code;
|
|
280
386
|
state.signal = signal;
|
|
281
387
|
state.timeUsed = Date.now() - start;
|
|
282
|
-
state.output = state.output.trim();
|
|
388
|
+
state.output = maskCurlOutput(command, state.output.trim());
|
|
283
389
|
// 更新当前目录(如果是 cd 命令且执行成功)
|
|
284
390
|
if (cdValidation.newDir && code === 0) {
|
|
285
391
|
sessionDirs.set(sessionId, cdValidation.newDir);
|
package/lib/locales/zh-CN.json
CHANGED
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { exec } from 'child_process'
|
|
2
2
|
import { Context, h, Schema, Time } from 'koishi'
|
|
3
|
+
import os from 'os'
|
|
3
4
|
import path from 'path'
|
|
4
5
|
import { pathToFileURL } from 'url'
|
|
5
6
|
import AnsiToHtml from 'ansi-to-html'
|
|
@@ -37,23 +38,7 @@ export const Config: Schema<Config> = Schema.object({
|
|
|
37
38
|
restrictDirectory: Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
|
|
38
39
|
authority: Schema.number().description('exec 命令所需权限等级。').default(4),
|
|
39
40
|
commandFilterMode: Schema.union(['blacklist', 'whitelist']).description('命令过滤模式:blacklist/whitelist').default('blacklist'),
|
|
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
|
+
commandList: Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([]),
|
|
57
42
|
})
|
|
58
43
|
|
|
59
44
|
export interface State {
|
|
@@ -99,12 +84,116 @@ function isCommandBlocked(command: string, mode: 'blacklist' | 'whitelist', list
|
|
|
99
84
|
return mode === 'blacklist' ? hit : !hit
|
|
100
85
|
}
|
|
101
86
|
|
|
87
|
+
function stripQuotes(text: string): string {
|
|
88
|
+
return text.replace(/^['"]|['"]$/g, '')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function tokenizeCommand(command: string): string[] {
|
|
92
|
+
const tokens: string[] = []
|
|
93
|
+
let current = ''
|
|
94
|
+
let quote: string | null = null
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < command.length; i++) {
|
|
97
|
+
const char = command[i]
|
|
98
|
+
|
|
99
|
+
if ((char === '"' || char === "'") && (quote === null || quote === char)) {
|
|
100
|
+
quote = quote ? null : char
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!quote && /\s/.test(char)) {
|
|
105
|
+
if (current) {
|
|
106
|
+
tokens.push(current)
|
|
107
|
+
current = ''
|
|
108
|
+
}
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
current += char
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (current) tokens.push(current)
|
|
116
|
+
return tokens
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isPathLike(token: string): boolean {
|
|
120
|
+
const trimmed = token.trim()
|
|
121
|
+
if (!trimmed) return false
|
|
122
|
+
if (/^[|&><]+$/.test(trimmed)) return false
|
|
123
|
+
if (/^-{1,2}[a-zA-Z0-9][\w-]*$/.test(trimmed)) return false
|
|
124
|
+
if (/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) return false
|
|
125
|
+
|
|
126
|
+
const normalized = stripQuotes(trimmed)
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
/^[A-Za-z]:[\\/]/.test(normalized) ||
|
|
130
|
+
normalized.startsWith('/') ||
|
|
131
|
+
normalized.startsWith('~') ||
|
|
132
|
+
normalized.startsWith('..') ||
|
|
133
|
+
normalized.startsWith('./') ||
|
|
134
|
+
normalized.includes('/') ||
|
|
135
|
+
normalized.includes('\\')
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveCandidatePath(candidate: string, currentDir: string): string {
|
|
140
|
+
const cleaned = stripQuotes(candidate.trim())
|
|
141
|
+
const homeDir = os.homedir?.() || ''
|
|
142
|
+
|
|
143
|
+
if (cleaned.startsWith('~')) {
|
|
144
|
+
const withoutTilde = cleaned.slice(1).replace(/^[/\\]/, '')
|
|
145
|
+
const homeResolved = homeDir ? path.join(homeDir, withoutTilde) : cleaned
|
|
146
|
+
return path.resolve(homeResolved)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return path.resolve(currentDir, cleaned)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function extractPathCandidates(command: string): string[] {
|
|
153
|
+
const tokens = tokenizeCommand(command)
|
|
154
|
+
const candidates: string[] = []
|
|
155
|
+
|
|
156
|
+
for (const token of tokens) {
|
|
157
|
+
const normalized = stripQuotes(token)
|
|
158
|
+
if (isPathLike(normalized)) {
|
|
159
|
+
candidates.push(normalized)
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const eqIndex = normalized.indexOf('=')
|
|
164
|
+
if (eqIndex > 0) {
|
|
165
|
+
const value = normalized.slice(eqIndex + 1)
|
|
166
|
+
if (isPathLike(value)) {
|
|
167
|
+
candidates.push(value)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return candidates
|
|
173
|
+
}
|
|
174
|
+
|
|
102
175
|
// 解析 cd 命令并验证路径
|
|
103
176
|
function isWithinRoot(rootDir: string, targetPath: string): boolean {
|
|
104
177
|
const relative = path.relative(rootDir, targetPath)
|
|
105
178
|
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
106
179
|
}
|
|
107
180
|
|
|
181
|
+
function validatePathAccess(command: string, currentDir: string, rootDir: string, restrictDirectory: boolean): { valid: boolean; error?: string } {
|
|
182
|
+
if (!restrictDirectory) return { valid: true }
|
|
183
|
+
|
|
184
|
+
const normalizedRoot = path.resolve(rootDir)
|
|
185
|
+
const candidates = extractPathCandidates(command)
|
|
186
|
+
|
|
187
|
+
for (const candidate of candidates) {
|
|
188
|
+
const resolved = resolveCandidatePath(candidate, currentDir)
|
|
189
|
+
if (!isWithinRoot(normalizedRoot, resolved)) {
|
|
190
|
+
return { valid: false, error: 'restricted-path' }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { valid: true }
|
|
195
|
+
}
|
|
196
|
+
|
|
108
197
|
function validateCdCommand(command: string, currentDir: string, rootDir: string, restrictDirectory: boolean): { valid: boolean; newDir?: string; error?: string } {
|
|
109
198
|
if (!restrictDirectory) return { valid: true }
|
|
110
199
|
|
|
@@ -138,6 +227,30 @@ function validateCdCommand(command: string, currentDir: string, rootDir: string,
|
|
|
138
227
|
return { valid: true }
|
|
139
228
|
}
|
|
140
229
|
|
|
230
|
+
function maskCurlOutput(command: string, output: string): string {
|
|
231
|
+
if (!output) return output
|
|
232
|
+
if (!/\bcurl\b/i.test(command)) return output
|
|
233
|
+
|
|
234
|
+
const 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
|
|
235
|
+
return output.replace(ipv4Regex, (ip) => (isPrivateIpv4(ip) ? ip : '*.*.*.*'))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isPrivateIpv4(ip: string): boolean {
|
|
239
|
+
const octets = ip.split('.').map(Number)
|
|
240
|
+
if (octets.length !== 4) return false
|
|
241
|
+
if (octets.some(octet => Number.isNaN(octet) || octet < 0 || octet > 255)) return false
|
|
242
|
+
|
|
243
|
+
const [a, b] = octets
|
|
244
|
+
|
|
245
|
+
if (a === 10) return true
|
|
246
|
+
if (a === 172 && b >= 16 && b <= 31) return true
|
|
247
|
+
if (a === 192 && b === 168) return true
|
|
248
|
+
if (a === 127) return true
|
|
249
|
+
if (a === 169 && b === 254) return true
|
|
250
|
+
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
|
|
141
254
|
// 渲染终端输出为图片
|
|
142
255
|
async function renderTerminalImage(ctx: Context, workingDir: string, command: string, output: string): Promise<h> {
|
|
143
256
|
if (!ctx.puppeteer) {
|
|
@@ -342,6 +455,10 @@ export function apply(ctx: Context, config: Config) {
|
|
|
342
455
|
if (!cdValidation.valid) {
|
|
343
456
|
return session.text('.restricted-directory')
|
|
344
457
|
}
|
|
458
|
+
const pathValidation = validatePathAccess(command, currentDir, rootDir, config.restrictDirectory)
|
|
459
|
+
if (!pathValidation.valid) {
|
|
460
|
+
return session.text('.restricted-path')
|
|
461
|
+
}
|
|
345
462
|
const { timeout } = config
|
|
346
463
|
const state: State = { command, timeout, output: '' }
|
|
347
464
|
if (!config.renderImage) {
|
|
@@ -366,7 +483,7 @@ export function apply(ctx: Context, config: Config) {
|
|
|
366
483
|
state.code = code
|
|
367
484
|
state.signal = signal
|
|
368
485
|
state.timeUsed = Date.now() - start
|
|
369
|
-
state.output = state.output.trim()
|
|
486
|
+
state.output = maskCurlOutput(command, state.output.trim())
|
|
370
487
|
// 更新当前目录(如果是 cd 命令且执行成功)
|
|
371
488
|
if (cdValidation.newDir && code === 0) {
|
|
372
489
|
sessionDirs.set(sessionId, cdValidation.newDir)
|
package/src/locales/zh-CN.yml
CHANGED