fileditor-mcp 1.0.2 → 1.0.3
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/1781792324448_PixelTunnelSender.exe +0 -0
- package/LICENSE +21 -21
- package/README.md +128 -128
- package/docs/MCP-INTERFACE.cn.md +975 -1061
- package/docs/MCP-INTERFACE.en.md +881 -974
- package/docs/README.cn.md +128 -128
- package/package.json +30 -31
- package/src/handlers/applyDiff.js +298 -324
- package/src/handlers/insertContent.js +179 -153
- package/src/handlers/listFiles.js +65 -54
- package/src/handlers/readFile.js +109 -109
- package/src/handlers/searchAndReplace.js +198 -217
- package/src/handlers/setWorkspace.js +26 -26
- package/src/handlers/writeFile.js +97 -91
- package/src/index.js +22 -22
- package/src/server.js +85 -85
- package/src/tools/toolDefinitions.js +291 -301
- package/src/utils/fileUtils.js +238 -716
- package/test/ApplyDiffHandler.test.js +754 -754
- package/test/InsertContentHandler.test.js +371 -369
- package/test/ListFilesHandler.test.js +302 -300
- package/test/ReadFileHandler.test.js +213 -213
- package/test/SearchAndReplaceHandler.test.js +505 -505
- package/test/SetWorkspaceHandler.test.js +290 -290
- package/test/WriteFileHandler.test.js +289 -289
- package/test/runAllTests.js +233 -233
|
@@ -1,290 +1,290 @@
|
|
|
1
|
-
import { SetWorkspaceHandler } from '../src/handlers/setWorkspace.js';
|
|
2
|
-
import { FileUtils } from '../src/utils/fileUtils.js';
|
|
3
|
-
import { describe, it, beforeEach, after } from 'node:test';
|
|
4
|
-
import assert from 'node:assert';
|
|
5
|
-
import { tmpdir } from 'os';
|
|
6
|
-
import { join } from 'path';
|
|
7
|
-
import { promises as fs } from 'fs';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Test suite for SetWorkspaceHandler.
|
|
11
|
-
* Tests the workspace setting functionality.
|
|
12
|
-
*/
|
|
13
|
-
describe('SetWorkspaceHandler', () => {
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
// Reset workspace state
|
|
17
|
-
FileUtils.setWorkspaceRoot(process.cwd());
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
describe('handle method', () => {
|
|
21
|
-
it('should successfully set the workspace root directory', async () => {
|
|
22
|
-
const testPath = process.cwd(); // Use the current directory, ensure it exists
|
|
23
|
-
const args = { path: testPath };
|
|
24
|
-
|
|
25
|
-
const result = await SetWorkspaceHandler.handle(args);
|
|
26
|
-
|
|
27
|
-
assert.strictEqual(result.content[0].type, 'text');
|
|
28
|
-
assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
|
|
29
|
-
assert.ok(result.content[0].text.includes(testPath));
|
|
30
|
-
assert.strictEqual(FileUtils.getWorkspaceRoot(), testPath);
|
|
31
|
-
}); it('should handle Windows path format', async () => {
|
|
32
|
-
if (process.platform !== 'win32') {
|
|
33
|
-
// Skip this test on non-Windows platforms
|
|
34
|
-
console.log('Skipping Windows path format test: not on a Windows platform');
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const testPath = 'C:\\'; // Use the root directory, usually exists on Windows
|
|
38
|
-
const args = { path: testPath };
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const result = await SetWorkspaceHandler.handle(args);
|
|
42
|
-
|
|
43
|
-
assert.strictEqual(result.content[0].type, 'text');
|
|
44
|
-
assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
|
|
45
|
-
} catch (error) {
|
|
46
|
-
// If C:\ does not exist (very rare), it should throw a directory not found error
|
|
47
|
-
assert.ok(error.message.includes('Workspace directory does not exist'));
|
|
48
|
-
}
|
|
49
|
-
}); it('should handle relative paths', async () => {
|
|
50
|
-
const testPath = '.'; // Use the current directory
|
|
51
|
-
const args = { path: testPath };
|
|
52
|
-
|
|
53
|
-
const result = await SetWorkspaceHandler.handle(args);
|
|
54
|
-
|
|
55
|
-
assert.strictEqual(result.content[0].type, 'text');
|
|
56
|
-
assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
|
|
57
|
-
// Relative paths are resolved to absolute paths
|
|
58
|
-
assert.ok(FileUtils.getWorkspaceRoot().includes(process.cwd()));
|
|
59
|
-
}); it('should throw an error when the directory does not exist', async () => {
|
|
60
|
-
const testPath = './nonexistent/directory';
|
|
61
|
-
const args = { path: testPath };
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
await SetWorkspaceHandler.handle(args);
|
|
65
|
-
assert.fail('Should have thrown an error');
|
|
66
|
-
} catch (error) {
|
|
67
|
-
assert.ok(error.message.includes('Workspace directory does not exist'));
|
|
68
|
-
}
|
|
69
|
-
}); it('should throw an error when the path contains a non-existent directory with special characters', async () => {
|
|
70
|
-
const testPath = '/path/with spaces/and-symbols_123';
|
|
71
|
-
const args = { path: testPath };
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
await SetWorkspaceHandler.handle(args);
|
|
75
|
-
assert.fail('Should have thrown an error');
|
|
76
|
-
} catch (error) {
|
|
77
|
-
assert.ok(error.message.includes('Workspace directory does not exist'));
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should throw an error when the path parameter is missing', async () => {
|
|
82
|
-
const args = {};
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
await SetWorkspaceHandler.handle(args);
|
|
86
|
-
assert.fail('Should have thrown an error');
|
|
87
|
-
} catch (error) {
|
|
88
|
-
assert.ok(error.message.includes('Failed to set workspace'));
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe('Edge cases', () => {
|
|
95
|
-
|
|
96
|
-
it('should handle null path', async () => {
|
|
97
|
-
const args = { path: null };
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
await SetWorkspaceHandler.handle(args);
|
|
101
|
-
// If no error is thrown, check the result
|
|
102
|
-
assert.strictEqual(FileUtils.getWorkspaceRoot(), null);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
// If an error is thrown, ensure it is a reasonable error message
|
|
105
|
-
assert.ok(error.message.includes('Failed to set workspace'));
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should handle undefined path', async () => {
|
|
110
|
-
const args = { path: undefined };
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
await SetWorkspaceHandler.handle(args);
|
|
114
|
-
assert.fail('Should have thrown an error');
|
|
115
|
-
} catch (error) {
|
|
116
|
-
assert.ok(error.message.includes('Failed to set workspace'));
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
describe('Path security tests', () => {
|
|
123
|
-
let testWorkspace;
|
|
124
|
-
|
|
125
|
-
beforeEach(async () => {
|
|
126
|
-
testWorkspace = join(tmpdir(), `mcp-security-test-${Date.now()}`);
|
|
127
|
-
await fs.mkdir(testWorkspace, { recursive: true });
|
|
128
|
-
|
|
129
|
-
// Create test directory structure
|
|
130
|
-
await fs.mkdir(join(testWorkspace, 'safe'), { recursive: true });
|
|
131
|
-
await fs.mkdir(join(testWorkspace, 'safe', 'nested'), { recursive: true });
|
|
132
|
-
await fs.writeFile(join(testWorkspace, 'safe', 'test.txt'), 'safe content');
|
|
133
|
-
await fs.writeFile(join(testWorkspace, 'safe', 'nested', 'deep.txt'), 'deep content');
|
|
134
|
-
|
|
135
|
-
// Create a file outside the workspace
|
|
136
|
-
await fs.writeFile(join(tmpdir(), 'outside.txt'), 'outside content');
|
|
137
|
-
|
|
138
|
-
FileUtils.setWorkspaceRoot(testWorkspace);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
after(async () => {
|
|
142
|
-
try {
|
|
143
|
-
await fs.rm(testWorkspace, { recursive: true, force: true });
|
|
144
|
-
await fs.rm(join(tmpdir(), 'outside.txt'), { force: true });
|
|
145
|
-
} catch (error) {
|
|
146
|
-
// Ignore cleanup errors
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('should succeed for normal path access', async () => {
|
|
151
|
-
// Test relative path
|
|
152
|
-
const safePath1 = FileUtils.getSecurePath('safe/test.txt');
|
|
153
|
-
assert.ok(safePath1.includes('test.txt'));
|
|
154
|
-
|
|
155
|
-
// Test nested path
|
|
156
|
-
const safePath2 = FileUtils.getSecurePath('safe/nested/deep.txt');
|
|
157
|
-
assert.ok(safePath2.includes('deep.txt'));
|
|
158
|
-
|
|
159
|
-
// Test current directory
|
|
160
|
-
const safePath3 = FileUtils.getSecurePath('.');
|
|
161
|
-
assert.strictEqual(safePath3, testWorkspace);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should prevent path traversal attacks', async () => {
|
|
165
|
-
const traversalAttempts = [
|
|
166
|
-
'../outside.txt', // Basic traversal
|
|
167
|
-
'../../outside.txt', // Deep traversal
|
|
168
|
-
'safe/../../../outside.txt', // Mixed traversal
|
|
169
|
-
'safe/../../outside.txt', // Nested traversal
|
|
170
|
-
'/etc/passwd', // Absolute path attack (Unix)
|
|
171
|
-
];
|
|
172
|
-
if (process.platform === 'win32') {
|
|
173
|
-
traversalAttempts.push(
|
|
174
|
-
'C:\\Windows\\System32\\config\\SAM', // Absolute path attack (Windows)
|
|
175
|
-
'..\\..\\outside.txt', // Windows-style traversal
|
|
176
|
-
'safe\\..\\..\\outside.txt', // Windows mixed traversal
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
for (const maliciousPath of traversalAttempts) {
|
|
180
|
-
await assert.rejects(
|
|
181
|
-
async () => FileUtils.getSecurePath(maliciousPath),
|
|
182
|
-
(error) => {
|
|
183
|
-
return error.message.includes('Access denied') ||
|
|
184
|
-
error.message.includes('outside the workspace');
|
|
185
|
-
},
|
|
186
|
-
`Path traversal attack should be blocked: ${maliciousPath}`
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('should reject dangerous characters', async () => {
|
|
192
|
-
const dangerousInputs = [
|
|
193
|
-
'test\0file.txt', // Null byte
|
|
194
|
-
'test\x00file.txt', // Hex null byte
|
|
195
|
-
'', // Empty string
|
|
196
|
-
null, // Null value
|
|
197
|
-
undefined, // Undefined value
|
|
198
|
-
123, // Number type
|
|
199
|
-
];
|
|
200
|
-
|
|
201
|
-
for (const dangerousInput of dangerousInputs) {
|
|
202
|
-
await assert.rejects(
|
|
203
|
-
async () => FileUtils.getSecurePath(dangerousInput),
|
|
204
|
-
(error) => {
|
|
205
|
-
return error.message.includes('Invalid file path') ||
|
|
206
|
-
error.message.includes('null bytes');
|
|
207
|
-
},
|
|
208
|
-
`Dangerous input should be rejected: ${dangerousInput}`
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// it('should prevent symlink traversal', async () => {
|
|
214
|
-
// // Create a symlink pointing outside the workspace
|
|
215
|
-
// const symlinkPath = join(testWorkspace, 'malicious_link');
|
|
216
|
-
// const outsidePath = join(tmpdir(), 'outside.txt');
|
|
217
|
-
//
|
|
218
|
-
// try {
|
|
219
|
-
// await fs.symlink(outsidePath, symlinkPath);
|
|
220
|
-
//
|
|
221
|
-
// // Attempt to access the file outside the workspace via the symlink
|
|
222
|
-
// await assert.rejects(
|
|
223
|
-
// async () => FileUtils.getSecurePath('malicious_link'),
|
|
224
|
-
// (error) => error.message.includes('Access denied') ||
|
|
225
|
-
// error.message.includes('outside the workspace'),
|
|
226
|
-
// 'Symlink traversal attack should be blocked'
|
|
227
|
-
// );
|
|
228
|
-
// } catch (error) {
|
|
229
|
-
// // Some systems may not support creating symlinks, skip this test
|
|
230
|
-
// if (error.code === 'EPERM' || error.code === 'ENOENT') {
|
|
231
|
-
// console.log('Skipping symlink test: system does not support or permission denied');
|
|
232
|
-
// return;
|
|
233
|
-
// }
|
|
234
|
-
// throw error;
|
|
235
|
-
// }
|
|
236
|
-
// });
|
|
237
|
-
|
|
238
|
-
it('should work correctly for workspace validation', async () => {
|
|
239
|
-
// Test invalid workspace settings
|
|
240
|
-
await assert.rejects(
|
|
241
|
-
async () => FileUtils.setWorkspaceRoot(''),
|
|
242
|
-
(error) => error.message.includes('Invalid workspace root'),
|
|
243
|
-
'Empty workspace path should be rejected'
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
await assert.rejects(
|
|
247
|
-
async () => FileUtils.setWorkspaceRoot(null),
|
|
248
|
-
(error) => error.message.includes('Invalid workspace root'),
|
|
249
|
-
'Null workspace path should be rejected'
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
await assert.rejects(
|
|
253
|
-
async () => FileUtils.setWorkspaceRoot('/nonexistent/directory'),
|
|
254
|
-
(error) => error.message.includes('does not exist'),
|
|
255
|
-
'Non-existent directory should be rejected'
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
// Test file as workspace
|
|
259
|
-
const tempFile = join(tmpdir(), `test-file-${Date.now()}.txt`);
|
|
260
|
-
await fs.writeFile(tempFile, 'test');
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
await assert.rejects(
|
|
264
|
-
async () => FileUtils.setWorkspaceRoot(tempFile),
|
|
265
|
-
(error) => error.message.includes('not a directory'),
|
|
266
|
-
'File path as workspace should be rejected'
|
|
267
|
-
);
|
|
268
|
-
} finally {
|
|
269
|
-
await fs.rm(tempFile, { force: true });
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('should deny access when workspace is not set', async () => {
|
|
274
|
-
// Temporarily clear workspace setting
|
|
275
|
-
const originalWorkspace = FileUtils.WORKSPACE_ROOT;
|
|
276
|
-
FileUtils.WORKSPACE_ROOT = null;
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
await assert.rejects(
|
|
280
|
-
async () => FileUtils.getSecurePath('test.txt'),
|
|
281
|
-
(error) => error.message.includes('Workspace not set'),
|
|
282
|
-
'Access should be denied when workspace is not set'
|
|
283
|
-
);
|
|
284
|
-
} finally {
|
|
285
|
-
// Restore workspace setting
|
|
286
|
-
FileUtils.WORKSPACE_ROOT = originalWorkspace;
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
});
|
|
1
|
+
import { SetWorkspaceHandler } from '../src/handlers/setWorkspace.js';
|
|
2
|
+
import { FileUtils } from '../src/utils/fileUtils.js';
|
|
3
|
+
import { describe, it, beforeEach, after } from 'node:test';
|
|
4
|
+
import assert from 'node:assert';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Test suite for SetWorkspaceHandler.
|
|
11
|
+
* Tests the workspace setting functionality.
|
|
12
|
+
*/
|
|
13
|
+
describe('SetWorkspaceHandler', () => {
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Reset workspace state
|
|
17
|
+
FileUtils.setWorkspaceRoot(process.cwd());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('handle method', () => {
|
|
21
|
+
it('should successfully set the workspace root directory', async () => {
|
|
22
|
+
const testPath = process.cwd(); // Use the current directory, ensure it exists
|
|
23
|
+
const args = { path: testPath };
|
|
24
|
+
|
|
25
|
+
const result = await SetWorkspaceHandler.handle(args);
|
|
26
|
+
|
|
27
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
28
|
+
assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
|
|
29
|
+
assert.ok(result.content[0].text.includes(testPath));
|
|
30
|
+
assert.strictEqual(FileUtils.getWorkspaceRoot(), testPath);
|
|
31
|
+
}); it('should handle Windows path format', async () => {
|
|
32
|
+
if (process.platform !== 'win32') {
|
|
33
|
+
// Skip this test on non-Windows platforms
|
|
34
|
+
console.log('Skipping Windows path format test: not on a Windows platform');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const testPath = 'C:\\'; // Use the root directory, usually exists on Windows
|
|
38
|
+
const args = { path: testPath };
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = await SetWorkspaceHandler.handle(args);
|
|
42
|
+
|
|
43
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
44
|
+
assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// If C:\ does not exist (very rare), it should throw a directory not found error
|
|
47
|
+
assert.ok(error.message.includes('Workspace directory does not exist'));
|
|
48
|
+
}
|
|
49
|
+
}); it('should handle relative paths', async () => {
|
|
50
|
+
const testPath = '.'; // Use the current directory
|
|
51
|
+
const args = { path: testPath };
|
|
52
|
+
|
|
53
|
+
const result = await SetWorkspaceHandler.handle(args);
|
|
54
|
+
|
|
55
|
+
assert.strictEqual(result.content[0].type, 'text');
|
|
56
|
+
assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
|
|
57
|
+
// Relative paths are resolved to absolute paths
|
|
58
|
+
assert.ok(FileUtils.getWorkspaceRoot().includes(process.cwd()));
|
|
59
|
+
}); it('should throw an error when the directory does not exist', async () => {
|
|
60
|
+
const testPath = './nonexistent/directory';
|
|
61
|
+
const args = { path: testPath };
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await SetWorkspaceHandler.handle(args);
|
|
65
|
+
assert.fail('Should have thrown an error');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
assert.ok(error.message.includes('Workspace directory does not exist'));
|
|
68
|
+
}
|
|
69
|
+
}); it('should throw an error when the path contains a non-existent directory with special characters', async () => {
|
|
70
|
+
const testPath = '/path/with spaces/and-symbols_123';
|
|
71
|
+
const args = { path: testPath };
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await SetWorkspaceHandler.handle(args);
|
|
75
|
+
assert.fail('Should have thrown an error');
|
|
76
|
+
} catch (error) {
|
|
77
|
+
assert.ok(error.message.includes('Workspace directory does not exist'));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should throw an error when the path parameter is missing', async () => {
|
|
82
|
+
const args = {};
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await SetWorkspaceHandler.handle(args);
|
|
86
|
+
assert.fail('Should have thrown an error');
|
|
87
|
+
} catch (error) {
|
|
88
|
+
assert.ok(error.message.includes('Failed to set workspace'));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('Edge cases', () => {
|
|
95
|
+
|
|
96
|
+
it('should handle null path', async () => {
|
|
97
|
+
const args = { path: null };
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await SetWorkspaceHandler.handle(args);
|
|
101
|
+
// If no error is thrown, check the result
|
|
102
|
+
assert.strictEqual(FileUtils.getWorkspaceRoot(), null);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// If an error is thrown, ensure it is a reasonable error message
|
|
105
|
+
assert.ok(error.message.includes('Failed to set workspace'));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle undefined path', async () => {
|
|
110
|
+
const args = { path: undefined };
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await SetWorkspaceHandler.handle(args);
|
|
114
|
+
assert.fail('Should have thrown an error');
|
|
115
|
+
} catch (error) {
|
|
116
|
+
assert.ok(error.message.includes('Failed to set workspace'));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Path security tests', () => {
|
|
123
|
+
let testWorkspace;
|
|
124
|
+
|
|
125
|
+
beforeEach(async () => {
|
|
126
|
+
testWorkspace = join(tmpdir(), `mcp-security-test-${Date.now()}`);
|
|
127
|
+
await fs.mkdir(testWorkspace, { recursive: true });
|
|
128
|
+
|
|
129
|
+
// Create test directory structure
|
|
130
|
+
await fs.mkdir(join(testWorkspace, 'safe'), { recursive: true });
|
|
131
|
+
await fs.mkdir(join(testWorkspace, 'safe', 'nested'), { recursive: true });
|
|
132
|
+
await fs.writeFile(join(testWorkspace, 'safe', 'test.txt'), 'safe content');
|
|
133
|
+
await fs.writeFile(join(testWorkspace, 'safe', 'nested', 'deep.txt'), 'deep content');
|
|
134
|
+
|
|
135
|
+
// Create a file outside the workspace
|
|
136
|
+
await fs.writeFile(join(tmpdir(), 'outside.txt'), 'outside content');
|
|
137
|
+
|
|
138
|
+
FileUtils.setWorkspaceRoot(testWorkspace);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
after(async () => {
|
|
142
|
+
try {
|
|
143
|
+
await fs.rm(testWorkspace, { recursive: true, force: true });
|
|
144
|
+
await fs.rm(join(tmpdir(), 'outside.txt'), { force: true });
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// Ignore cleanup errors
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should succeed for normal path access', async () => {
|
|
151
|
+
// Test relative path
|
|
152
|
+
const safePath1 = FileUtils.getSecurePath('safe/test.txt');
|
|
153
|
+
assert.ok(safePath1.includes('test.txt'));
|
|
154
|
+
|
|
155
|
+
// Test nested path
|
|
156
|
+
const safePath2 = FileUtils.getSecurePath('safe/nested/deep.txt');
|
|
157
|
+
assert.ok(safePath2.includes('deep.txt'));
|
|
158
|
+
|
|
159
|
+
// Test current directory
|
|
160
|
+
const safePath3 = FileUtils.getSecurePath('.');
|
|
161
|
+
assert.strictEqual(safePath3, testWorkspace);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should prevent path traversal attacks', async () => {
|
|
165
|
+
const traversalAttempts = [
|
|
166
|
+
'../outside.txt', // Basic traversal
|
|
167
|
+
'../../outside.txt', // Deep traversal
|
|
168
|
+
'safe/../../../outside.txt', // Mixed traversal
|
|
169
|
+
'safe/../../outside.txt', // Nested traversal
|
|
170
|
+
'/etc/passwd', // Absolute path attack (Unix)
|
|
171
|
+
];
|
|
172
|
+
if (process.platform === 'win32') {
|
|
173
|
+
traversalAttempts.push(
|
|
174
|
+
'C:\\Windows\\System32\\config\\SAM', // Absolute path attack (Windows)
|
|
175
|
+
'..\\..\\outside.txt', // Windows-style traversal
|
|
176
|
+
'safe\\..\\..\\outside.txt', // Windows mixed traversal
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
for (const maliciousPath of traversalAttempts) {
|
|
180
|
+
await assert.rejects(
|
|
181
|
+
async () => FileUtils.getSecurePath(maliciousPath),
|
|
182
|
+
(error) => {
|
|
183
|
+
return error.message.includes('Access denied') ||
|
|
184
|
+
error.message.includes('outside the workspace');
|
|
185
|
+
},
|
|
186
|
+
`Path traversal attack should be blocked: ${maliciousPath}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should reject dangerous characters', async () => {
|
|
192
|
+
const dangerousInputs = [
|
|
193
|
+
'test\0file.txt', // Null byte
|
|
194
|
+
'test\x00file.txt', // Hex null byte
|
|
195
|
+
'', // Empty string
|
|
196
|
+
null, // Null value
|
|
197
|
+
undefined, // Undefined value
|
|
198
|
+
123, // Number type
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
for (const dangerousInput of dangerousInputs) {
|
|
202
|
+
await assert.rejects(
|
|
203
|
+
async () => FileUtils.getSecurePath(dangerousInput),
|
|
204
|
+
(error) => {
|
|
205
|
+
return error.message.includes('Invalid file path') ||
|
|
206
|
+
error.message.includes('null bytes');
|
|
207
|
+
},
|
|
208
|
+
`Dangerous input should be rejected: ${dangerousInput}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// it('should prevent symlink traversal', async () => {
|
|
214
|
+
// // Create a symlink pointing outside the workspace
|
|
215
|
+
// const symlinkPath = join(testWorkspace, 'malicious_link');
|
|
216
|
+
// const outsidePath = join(tmpdir(), 'outside.txt');
|
|
217
|
+
//
|
|
218
|
+
// try {
|
|
219
|
+
// await fs.symlink(outsidePath, symlinkPath);
|
|
220
|
+
//
|
|
221
|
+
// // Attempt to access the file outside the workspace via the symlink
|
|
222
|
+
// await assert.rejects(
|
|
223
|
+
// async () => FileUtils.getSecurePath('malicious_link'),
|
|
224
|
+
// (error) => error.message.includes('Access denied') ||
|
|
225
|
+
// error.message.includes('outside the workspace'),
|
|
226
|
+
// 'Symlink traversal attack should be blocked'
|
|
227
|
+
// );
|
|
228
|
+
// } catch (error) {
|
|
229
|
+
// // Some systems may not support creating symlinks, skip this test
|
|
230
|
+
// if (error.code === 'EPERM' || error.code === 'ENOENT') {
|
|
231
|
+
// console.log('Skipping symlink test: system does not support or permission denied');
|
|
232
|
+
// return;
|
|
233
|
+
// }
|
|
234
|
+
// throw error;
|
|
235
|
+
// }
|
|
236
|
+
// });
|
|
237
|
+
|
|
238
|
+
it('should work correctly for workspace validation', async () => {
|
|
239
|
+
// Test invalid workspace settings
|
|
240
|
+
await assert.rejects(
|
|
241
|
+
async () => FileUtils.setWorkspaceRoot(''),
|
|
242
|
+
(error) => error.message.includes('Invalid workspace root'),
|
|
243
|
+
'Empty workspace path should be rejected'
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
await assert.rejects(
|
|
247
|
+
async () => FileUtils.setWorkspaceRoot(null),
|
|
248
|
+
(error) => error.message.includes('Invalid workspace root'),
|
|
249
|
+
'Null workspace path should be rejected'
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
await assert.rejects(
|
|
253
|
+
async () => FileUtils.setWorkspaceRoot('/nonexistent/directory'),
|
|
254
|
+
(error) => error.message.includes('does not exist'),
|
|
255
|
+
'Non-existent directory should be rejected'
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Test file as workspace
|
|
259
|
+
const tempFile = join(tmpdir(), `test-file-${Date.now()}.txt`);
|
|
260
|
+
await fs.writeFile(tempFile, 'test');
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await assert.rejects(
|
|
264
|
+
async () => FileUtils.setWorkspaceRoot(tempFile),
|
|
265
|
+
(error) => error.message.includes('not a directory'),
|
|
266
|
+
'File path as workspace should be rejected'
|
|
267
|
+
);
|
|
268
|
+
} finally {
|
|
269
|
+
await fs.rm(tempFile, { force: true });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should deny access when workspace is not set', async () => {
|
|
274
|
+
// Temporarily clear workspace setting
|
|
275
|
+
const originalWorkspace = FileUtils.WORKSPACE_ROOT;
|
|
276
|
+
FileUtils.WORKSPACE_ROOT = null;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
await assert.rejects(
|
|
280
|
+
async () => FileUtils.getSecurePath('test.txt'),
|
|
281
|
+
(error) => error.message.includes('Workspace not set'),
|
|
282
|
+
'Access should be denied when workspace is not set'
|
|
283
|
+
);
|
|
284
|
+
} finally {
|
|
285
|
+
// Restore workspace setting
|
|
286
|
+
FileUtils.WORKSPACE_ROOT = originalWorkspace;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|