ruvector 0.1.98 → 0.1.100

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 (2) hide show
  1. package/bin/mcp-server.js +71 -18
  2. package/package.json +1 -1
package/bin/mcp-server.js CHANGED
@@ -36,18 +36,38 @@ function validateRvfPath(filePath) {
36
36
  if (typeof filePath !== 'string' || filePath.length === 0) {
37
37
  throw new Error('Path must be a non-empty string');
38
38
  }
39
- const resolved = path.resolve(filePath);
40
- // Block obvious path traversal
41
- if (filePath.includes('..') || filePath.includes('\0')) {
42
- throw new Error('Path traversal detected');
39
+ // Block null bytes
40
+ if (filePath.includes('\0')) {
41
+ throw new Error('Path contains null bytes');
43
42
  }
44
- // Block sensitive system paths
45
- const blocked = ['/etc', '/proc', '/sys', '/dev', '/boot', '/root', '/var/run'];
46
- for (const prefix of blocked) {
47
- if (resolved.startsWith(prefix)) {
48
- throw new Error(`Access to ${prefix} is not allowed`);
43
+ // Resolve to absolute, then canonicalize via realpath if it exists
44
+ let resolved = path.resolve(filePath);
45
+ try {
46
+ // Resolve symlinks for existing paths to prevent symlink-based escapes
47
+ resolved = fs.realpathSync(resolved);
48
+ } catch {
49
+ // Path doesn't exist yet — resolve the parent directory
50
+ const parentDir = path.dirname(resolved);
51
+ try {
52
+ const realParent = fs.realpathSync(parentDir);
53
+ resolved = path.join(realParent, path.basename(resolved));
54
+ } catch {
55
+ // Parent doesn't exist either — keep the resolved path for the block check
49
56
  }
50
57
  }
58
+ // Confine to the current working directory
59
+ const cwd = process.cwd();
60
+ if (!resolved.startsWith(cwd + path.sep) && resolved !== cwd) {
61
+ // Also block sensitive system paths regardless
62
+ const blocked = ['/etc', '/proc', '/sys', '/dev', '/boot', '/root', '/var/run', '/var/log', '/tmp'];
63
+ for (const prefix of blocked) {
64
+ if (resolved.startsWith(prefix)) {
65
+ throw new Error(`Access denied: path resolves to '${resolved}' which is outside the working directory and in restricted area '${prefix}'`);
66
+ }
67
+ }
68
+ // Allow paths outside cwd only if they're not in blocked directories
69
+ // (for tools that reference project files by absolute path)
70
+ }
51
71
  return resolved;
52
72
  }
53
73
 
@@ -57,14 +77,24 @@ function validateRvfPath(filePath) {
57
77
  */
58
78
  function sanitizeShellArg(arg) {
59
79
  if (typeof arg !== 'string') return '';
60
- // Remove null bytes, backticks, $(), and other shell metacharacters
80
+ // Remove null bytes, backticks, $(), quotes, newlines, and other shell metacharacters
61
81
  return arg
62
82
  .replace(/\0/g, '')
63
- .replace(/[`$(){}|;&<>!]/g, '')
83
+ .replace(/[\r\n]/g, '')
84
+ .replace(/[`$(){}|;&<>!'"\\]/g, '')
64
85
  .replace(/\.\./g, '')
65
86
  .slice(0, 4096);
66
87
  }
67
88
 
89
+ /**
90
+ * Validate a numeric argument (returns integer or default).
91
+ * Prevents injection via numeric-looking fields.
92
+ */
93
+ function sanitizeNumericArg(arg, defaultVal) {
94
+ const n = parseInt(arg, 10);
95
+ return Number.isFinite(n) && n > 0 ? n : (defaultVal || 0);
96
+ }
97
+
68
98
  // Try to load the full IntelligenceEngine
69
99
  let IntelligenceEngine = null;
70
100
  let engineAvailable = false;
@@ -1319,7 +1349,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1319
1349
  let cmd = 'npx ruvector hooks init';
1320
1350
  if (args.force) cmd += ' --force';
1321
1351
  if (args.pretrain) cmd += ' --pretrain';
1322
- if (args.build_agents) cmd += ` --build-agents ${args.build_agents}`;
1352
+ if (args.build_agents) cmd += ` --build-agents ${sanitizeShellArg(args.build_agents)}`;
1323
1353
 
1324
1354
  try {
1325
1355
  const output = execSync(cmd, { encoding: 'utf-8', timeout: 60000 });
@@ -1341,7 +1371,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1341
1371
 
1342
1372
  case 'hooks_pretrain': {
1343
1373
  let cmd = 'npx ruvector hooks pretrain';
1344
- if (args.depth) cmd += ` --depth ${args.depth}`;
1374
+ if (args.depth) cmd += ` --depth ${sanitizeNumericArg(args.depth, 3)}`;
1345
1375
  if (args.skip_git) cmd += ' --skip-git';
1346
1376
  if (args.verbose) cmd += ' --verbose';
1347
1377
 
@@ -1371,7 +1401,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1371
1401
 
1372
1402
  case 'hooks_build_agents': {
1373
1403
  let cmd = 'npx ruvector hooks build-agents';
1374
- if (args.focus) cmd += ` --focus ${args.focus}`;
1404
+ if (args.focus) cmd += ` --focus ${sanitizeShellArg(args.focus)}`;
1375
1405
  if (args.include_prompts) cmd += ' --include-prompts';
1376
1406
 
1377
1407
  try {
@@ -1484,21 +1514,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1484
1514
  const data = args.data;
1485
1515
  const merge = args.merge !== false;
1486
1516
 
1487
- if (data.patterns) {
1517
+ // Validate imported data structure to prevent prototype pollution and injection
1518
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
1519
+ throw new Error('Import data must be a non-null object');
1520
+ }
1521
+ const allowedKeys = ['patterns', 'memories', 'errors', 'agents', 'edges', 'trajectories'];
1522
+ for (const key of Object.keys(data)) {
1523
+ if (!allowedKeys.includes(key)) {
1524
+ throw new Error(`Unknown import key: '${key}'. Allowed: ${allowedKeys.join(', ')}`);
1525
+ }
1526
+ }
1527
+ // Prevent prototype pollution via __proto__, constructor, prototype keys
1528
+ const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
1529
+ function checkForProtoPollution(obj, path) {
1530
+ if (typeof obj !== 'object' || obj === null) return;
1531
+ for (const key of Object.keys(obj)) {
1532
+ if (dangerousKeys.includes(key)) {
1533
+ throw new Error(`Dangerous key '${key}' detected at ${path}.${key}`);
1534
+ }
1535
+ }
1536
+ }
1537
+ if (data.patterns) checkForProtoPollution(data.patterns, 'patterns');
1538
+ if (data.errors) checkForProtoPollution(data.errors, 'errors');
1539
+
1540
+ if (data.patterns && typeof data.patterns === 'object') {
1488
1541
  if (merge) {
1489
1542
  Object.assign(intel.data.patterns, data.patterns);
1490
1543
  } else {
1491
1544
  intel.data.patterns = data.patterns;
1492
1545
  }
1493
1546
  }
1494
- if (data.memories) {
1547
+ if (data.memories && Array.isArray(data.memories)) {
1495
1548
  if (merge) {
1496
1549
  intel.data.memories = [...(intel.data.memories || []), ...data.memories];
1497
1550
  } else {
1498
1551
  intel.data.memories = data.memories;
1499
1552
  }
1500
1553
  }
1501
- if (data.errors) {
1554
+ if (data.errors && typeof data.errors === 'object') {
1502
1555
  if (merge) {
1503
1556
  Object.assign(intel.data.errors, data.errors);
1504
1557
  } else {
@@ -2426,7 +2479,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2426
2479
 
2427
2480
  case 'workers_status': {
2428
2481
  try {
2429
- const cmdArgs = args.workerId ? `workers status ${args.workerId}` : 'workers status';
2482
+ const cmdArgs = args.workerId ? `workers status ${sanitizeShellArg(args.workerId)}` : 'workers status';
2430
2483
  const result = execSync(`npx agentic-flow@alpha ${cmdArgs}`, {
2431
2484
  encoding: 'utf-8',
2432
2485
  timeout: 15000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvector",
3
- "version": "0.1.98",
3
+ "version": "0.1.100",
4
4
  "description": "High-performance vector database for Node.js with automatic native/WASM fallback",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",