file-organizer-mcp 2.1.0
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/LICENSE +21 -0
- package/README.md +603 -0
- package/package.json +34 -0
- package/server.js +668 -0
- package/server.json +48 -0
- package/start.bat +3 -0
- package/test_security.js +205 -0
package/start.bat
ADDED
package/test_security.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { FileOrganizerServer } from './server.js';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
class SecurityTester {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.testsPassed = 0;
|
|
14
|
+
this.testsFailed = 0;
|
|
15
|
+
this.server = new FileOrganizerServer();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async test(name, fn) {
|
|
19
|
+
try {
|
|
20
|
+
await fn();
|
|
21
|
+
console.log(`✅ PASS: ${name}`);
|
|
22
|
+
this.testsPassed++;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.log(`❌ FAIL: ${name}`);
|
|
25
|
+
console.log(` Error: ${error.message}`);
|
|
26
|
+
this.testsFailed++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async runTests() {
|
|
31
|
+
console.log('🔒 Running Security Tests...\n');
|
|
32
|
+
|
|
33
|
+
// Test 1: Path Traversal Attack
|
|
34
|
+
await this.test('Sanitize or Reject path traversal with ..', async () => {
|
|
35
|
+
const maliciousPath = '../../../etc/passwd';
|
|
36
|
+
try {
|
|
37
|
+
const resolved = await this.server.validatePath(maliciousPath);
|
|
38
|
+
// If it returns, it MUST be inside CWD
|
|
39
|
+
const cwd = process.cwd();
|
|
40
|
+
if (!resolved.startsWith(cwd)) {
|
|
41
|
+
throw new Error(`Path escaped CWD: ${resolved}`);
|
|
42
|
+
}
|
|
43
|
+
console.log(` (Sanitized to: ${resolved})`);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (!e.message.includes('Access denied')) throw e;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Test 2: Symlink Attack
|
|
50
|
+
await this.test('Reject symlink outside CWD', async () => {
|
|
51
|
+
// Only run on systems that support symlinks easily or mock it
|
|
52
|
+
// We'll try to create a symlink to parent dir
|
|
53
|
+
const target = path.join(__dirname, '..');
|
|
54
|
+
const linkPath = path.join(__dirname, 'symlink_test');
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await fs.symlink(target, linkPath, 'dir');
|
|
58
|
+
try {
|
|
59
|
+
await this.server.validatePath(linkPath);
|
|
60
|
+
throw new Error('Should have rejected outside symlink');
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if (!e.message.includes('Access denied')) throw e;
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
// Windows requires admin for symlinks usually, so this might fail to create symlink
|
|
66
|
+
// We'll skip if symlink creation fails
|
|
67
|
+
if (e.code === 'EPERM') {
|
|
68
|
+
console.log(' (Skipped symlink test due to permissions)');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
throw e;
|
|
72
|
+
} finally {
|
|
73
|
+
try { await fs.unlink(linkPath); } catch { }
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Test 3: Large File Handling
|
|
78
|
+
await this.test('Skip files larger than MAX_FILE_SIZE', async () => {
|
|
79
|
+
// Temporarily lower limit to 1MB for testing
|
|
80
|
+
this.server.MAX_FILE_SIZE = 1024 * 1024;
|
|
81
|
+
|
|
82
|
+
const testFile = path.join(__dirname, 'large_test_file.bin');
|
|
83
|
+
const size = 2 * 1024 * 1024; // 2MB
|
|
84
|
+
|
|
85
|
+
// Create sparse file
|
|
86
|
+
const fh = await fs.open(testFile, 'w');
|
|
87
|
+
await fh.truncate(size);
|
|
88
|
+
await fh.close();
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await this.server.calculateFileHash(testFile);
|
|
92
|
+
throw new Error('Should have rejected large file');
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (!e.message.includes('exceeds maximum size')) throw e;
|
|
95
|
+
} finally {
|
|
96
|
+
await fs.unlink(testFile);
|
|
97
|
+
// Restore limit
|
|
98
|
+
this.server.MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Test 3b: Graceful Duplicate Finding with Large Files
|
|
103
|
+
await this.test('Gracefully handle large files in duplicate find', async () => {
|
|
104
|
+
// Lower limit
|
|
105
|
+
this.server.MAX_FILE_SIZE = 1024 * 1024; // 1MB
|
|
106
|
+
const testDir = path.join(__dirname, 'dup_test_large');
|
|
107
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
108
|
+
|
|
109
|
+
const largeFile = path.join(testDir, 'large.bin');
|
|
110
|
+
const fd = await fs.open(largeFile, 'w');
|
|
111
|
+
await fd.truncate(2 * 1024 * 1024); // 2MB
|
|
112
|
+
await fd.close();
|
|
113
|
+
|
|
114
|
+
const smallFile = path.join(testDir, 'small.txt');
|
|
115
|
+
await fs.writeFile(smallFile, 'test content');
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// This should NOT throw, just log error for large file and process small one
|
|
119
|
+
const result = await this.server.findDuplicateFiles(testDir);
|
|
120
|
+
// Ensure result is valid
|
|
121
|
+
if (!result || !result.content) throw new Error('No result returned');
|
|
122
|
+
} finally {
|
|
123
|
+
await fs.rm(testDir, { recursive: true });
|
|
124
|
+
this.server.MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Test 4: Deep Directory Scan
|
|
129
|
+
await this.test('Enforce MAX_DEPTH limit', async () => {
|
|
130
|
+
// Lower limit to 3 for testing
|
|
131
|
+
this.server.MAX_DEPTH = 3;
|
|
132
|
+
|
|
133
|
+
const baseDir = path.join(__dirname, 'deep_test');
|
|
134
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
135
|
+
|
|
136
|
+
let currentPath = baseDir;
|
|
137
|
+
for (let i = 0; i < 5; i++) {
|
|
138
|
+
currentPath = path.join(currentPath, `level_${i}`);
|
|
139
|
+
await fs.mkdir(currentPath, { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// This shouldn't throw, but it should limit recursion
|
|
143
|
+
// We can verify by checking output or just ensuring it doesn't crash/hang
|
|
144
|
+
// The implementation prints a warning and returns.
|
|
145
|
+
// We'll scan and see if deeper files are missing if we were to check results,
|
|
146
|
+
// but for now checking it doesn't error is a basic check.
|
|
147
|
+
// Ideally we'd modify scanDirectory to return results and check them.
|
|
148
|
+
|
|
149
|
+
// Monkey patch console.error to catch the warning
|
|
150
|
+
let warningCaught = false;
|
|
151
|
+
const originalError = console.error;
|
|
152
|
+
console.error = (msg) => {
|
|
153
|
+
if (msg.includes('Max depth')) warningCaught = true;
|
|
154
|
+
// originalError(msg); // suppress output
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await this.server.scanDirectory(baseDir, true);
|
|
159
|
+
if (!warningCaught) throw new Error('Should have triggered max depth warning');
|
|
160
|
+
} finally {
|
|
161
|
+
console.error = originalError;
|
|
162
|
+
await fs.rm(baseDir, { recursive: true });
|
|
163
|
+
this.server.MAX_DEPTH = 10;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Test 5: File Count Limit
|
|
168
|
+
await this.test('Enforce MAX_FILES limit', async () => {
|
|
169
|
+
// Lower limit to 5
|
|
170
|
+
this.server.MAX_FILES = 5;
|
|
171
|
+
|
|
172
|
+
const testDir = path.join(__dirname, 'many_files_test');
|
|
173
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < 10; i++) {
|
|
176
|
+
await fs.writeFile(path.join(testDir, `file_${i}.txt`), 'test');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await this.server.scanDirectory(testDir, false); // non-recursive scan also checks limit in my impl?
|
|
181
|
+
// Wait, my impl check limits in SCAN directory loops.
|
|
182
|
+
// listFiles impl didn't get the limit check in my multi_replace?
|
|
183
|
+
// I added limits to scanDirectory inner loop.
|
|
184
|
+
// Let's check scanDirectory.
|
|
185
|
+
throw new Error('Should have thrown MAX_FILES limit error');
|
|
186
|
+
} catch (e) {
|
|
187
|
+
if (!e.message.includes('Maximum file limit')) throw e;
|
|
188
|
+
} finally {
|
|
189
|
+
await fs.rm(testDir, { recursive: true });
|
|
190
|
+
this.server.MAX_FILES = 10000;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Summary
|
|
195
|
+
console.log('\n' + '='.repeat(50));
|
|
196
|
+
console.log(`Tests Passed: ${this.testsPassed}`);
|
|
197
|
+
console.log(`Tests Failed: ${this.testsFailed}`);
|
|
198
|
+
console.log('='.repeat(50));
|
|
199
|
+
|
|
200
|
+
process.exit(this.testsFailed > 0 ? 1 : 0);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const tester = new SecurityTester();
|
|
205
|
+
tester.runTests().catch(console.error);
|