clawvault 2.4.7 → 2.5.1

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.
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'child_process';
2
2
  import chalk from 'chalk';
3
+ import path from 'path';
3
4
  import {
4
5
  ClawVault,
5
6
  QmdUnavailableError,
@@ -7,6 +8,38 @@ import {
7
8
  resolveVaultPath as resolveConfiguredVaultPath
8
9
  } from '../dist/index.js';
9
10
 
11
+ /**
12
+ * Validates that a path is within an allowed base directory.
13
+ * Prevents path traversal attacks.
14
+ * @param {string} inputPath - The path to validate
15
+ * @param {string} basePath - The allowed base directory
16
+ * @returns {string} The resolved, validated path
17
+ * @throws {Error} If the path escapes the base directory
18
+ */
19
+ export function validatePathWithinBase(inputPath, basePath) {
20
+ const resolvedBase = path.resolve(basePath);
21
+ const resolvedPath = path.resolve(basePath, inputPath);
22
+
23
+ if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
24
+ throw new Error(`Path traversal detected: ${inputPath} escapes ${basePath}`);
25
+ }
26
+
27
+ return resolvedPath;
28
+ }
29
+
30
+ /**
31
+ * Sanitizes an argument that may contain a path to prevent injection.
32
+ * @param {string} arg - The argument to sanitize
33
+ * @returns {string} The sanitized argument
34
+ */
35
+ function sanitizeQmdArg(arg) {
36
+ // Reject arguments with null bytes (injection attempt)
37
+ if (arg.includes('\0')) {
38
+ throw new Error('Invalid argument: contains null byte');
39
+ }
40
+ return arg;
41
+ }
42
+
10
43
  export function resolveVaultPath(vaultPath) {
11
44
  return resolveConfiguredVaultPath({ explicitPath: vaultPath });
12
45
  }
@@ -19,7 +52,9 @@ export async function getVault(vaultPath) {
19
52
 
20
53
  export async function runQmd(args) {
21
54
  return new Promise((resolve, reject) => {
22
- const proc = spawn('qmd', args, { stdio: 'inherit' });
55
+ // Sanitize all arguments before passing to spawn
56
+ const sanitizedArgs = args.map(sanitizeQmdArg);
57
+ const proc = spawn('qmd', sanitizedArgs, { stdio: 'inherit' });
23
58
  proc.on('close', (code) => {
24
59
  if (code === 0) resolve();
25
60
  else reject(new Error(`qmd exited with code ${code}`));
@@ -2,6 +2,8 @@
2
2
  * Core vault lifecycle and write command registrations.
3
3
  */
4
4
 
5
+ import { validatePathWithinBase } from './command-runtime.js';
6
+
5
7
  export function registerCoreCommands(
6
8
  program,
7
9
  { chalk, path, fs, createVault, getVault, runQmd }
@@ -179,7 +181,10 @@ export function registerCoreCommands(
179
181
  let content = options.content || '';
180
182
 
181
183
  if (options.file) {
182
- content = fs.readFileSync(options.file, 'utf-8');
184
+ // Validate file path is within current working directory to prevent path traversal
185
+ const cwd = process.cwd();
186
+ const resolvedFilePath = validatePathWithinBase(options.file, cwd);
187
+ content = fs.readFileSync(resolvedFilePath, 'utf-8');
183
188
  } else if (options.stdin) {
184
189
  content = fs.readFileSync(0, 'utf-8');
185
190
  }
@@ -2,6 +2,8 @@
2
2
  * Vault operation command registrations (browse/sync/reindex/remember/shell-init/dashboard).
3
3
  */
4
4
 
5
+ import { validatePathWithinBase } from './command-runtime.js';
6
+
5
7
  export function registerVaultOperationsCommands(
6
8
  program,
7
9
  {
@@ -232,7 +234,10 @@ export function registerVaultOperationsCommands(
232
234
  const vault = await getVault(options.vault);
233
235
  let content = options.content || '';
234
236
  if (options.file) {
235
- content = fs.readFileSync(options.file, 'utf-8');
237
+ // Validate file path is within current working directory to prevent path traversal
238
+ const cwd = process.cwd();
239
+ const resolvedFilePath = validatePathWithinBase(options.file, cwd);
240
+ content = fs.readFileSync(resolvedFilePath, 'utf-8');
236
241
  } else if (options.stdin) {
237
242
  content = fs.readFileSync(0, 'utf-8');
238
243
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawvault",
3
- "version": "2.4.7",
3
+ "version": "2.5.1",
4
4
  "description": "Structured memory system for AI agents — typed storage, knowledge graph, context profiles, canvas dashboards, neural graph themes, and Obsidian-native task views. An elephant never forgets. 🐘",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",