claudehq 1.0.2 → 1.0.5

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.
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Path Validator - Security utilities for path validation
3
+ *
4
+ * Validates directory paths for safety before use in shell commands.
5
+ * Prevents command injection, path traversal, and other attacks.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ // Characters that could be dangerous in shell commands
13
+ // Note: ~ is allowed as it's used for home directory expansion
14
+ const SHELL_METACHARACTERS = /[;&|`$(){}[\]<>\\!#*?\n\r\x00]/;
15
+
16
+ // Patterns that indicate path traversal attempts
17
+ const PATH_TRAVERSAL_PATTERNS = [
18
+ /\.\.\//, // ../
19
+ /\.\.\\/, // ..\
20
+ /\.\.$/, // ends with ..
21
+ /^\.\.$/ // just ..
22
+ ];
23
+
24
+ // Maximum path length (OS limit is usually 4096, but we're conservative)
25
+ const MAX_PATH_LENGTH = 1024;
26
+
27
+ /**
28
+ * Validate a tmux session name
29
+ * @param {string} name - Session name to validate
30
+ * @returns {boolean} True if valid
31
+ */
32
+ function isValidTmuxSessionName(name) {
33
+ if (!name || typeof name !== 'string') {
34
+ return false;
35
+ }
36
+ // tmux session names can only contain alphanumeric, underscore, hyphen
37
+ return /^[a-zA-Z0-9_-]+$/.test(name);
38
+ }
39
+
40
+ /**
41
+ * Validate a session name for safe use
42
+ * @param {string} name - Session name to validate
43
+ * @throws {Error} If validation fails
44
+ * @returns {string} The validated name
45
+ */
46
+ function validateTmuxSessionName(name) {
47
+ if (!isValidTmuxSessionName(name)) {
48
+ throw new Error('Invalid tmux session name: must contain only alphanumeric characters, underscores, and hyphens');
49
+ }
50
+ return name;
51
+ }
52
+
53
+ /**
54
+ * Check if a path contains shell metacharacters
55
+ * @param {string} pathStr - Path to check
56
+ * @returns {boolean} True if contains dangerous characters
57
+ */
58
+ function containsShellMetacharacters(pathStr) {
59
+ return SHELL_METACHARACTERS.test(pathStr);
60
+ }
61
+
62
+ /**
63
+ * Check if a path appears to be a path traversal attempt
64
+ * @param {string} pathStr - Path to check
65
+ * @returns {boolean} True if appears to be traversal attempt
66
+ */
67
+ function isPathTraversalAttempt(pathStr) {
68
+ return PATH_TRAVERSAL_PATTERNS.some(pattern => pattern.test(pathStr));
69
+ }
70
+
71
+ /**
72
+ * Normalize and resolve a path, expanding ~ for home directory
73
+ * @param {string} pathStr - Path to normalize
74
+ * @returns {string} Normalized absolute path
75
+ */
76
+ function normalizePath(pathStr) {
77
+ if (!pathStr || typeof pathStr !== 'string') {
78
+ return '';
79
+ }
80
+
81
+ // Expand ~ to home directory
82
+ if (pathStr.startsWith('~')) {
83
+ pathStr = path.join(os.homedir(), pathStr.slice(1));
84
+ }
85
+
86
+ // Resolve to absolute path
87
+ return path.resolve(pathStr);
88
+ }
89
+
90
+ /**
91
+ * Validate a directory path for safe use
92
+ * @param {string} dirPath - Directory path to validate
93
+ * @param {Object} options - Validation options
94
+ * @param {boolean} options.mustExist - If true, directory must exist (default: true)
95
+ * @param {boolean} options.allowCreate - If true, allows non-existent but valid paths (default: false)
96
+ * @returns {Object} Validation result { valid: boolean, path: string, error: string|null }
97
+ */
98
+ function validateDirectoryPath(dirPath, options = {}) {
99
+ const { mustExist = true, allowCreate = false } = options;
100
+
101
+ // Check for null/undefined/empty
102
+ if (!dirPath || typeof dirPath !== 'string') {
103
+ return {
104
+ valid: false,
105
+ path: null,
106
+ error: 'Directory path is required'
107
+ };
108
+ }
109
+
110
+ // Trim whitespace
111
+ const trimmed = dirPath.trim();
112
+ if (!trimmed) {
113
+ return {
114
+ valid: false,
115
+ path: null,
116
+ error: 'Directory path cannot be empty'
117
+ };
118
+ }
119
+
120
+ // Check path length
121
+ if (trimmed.length > MAX_PATH_LENGTH) {
122
+ return {
123
+ valid: false,
124
+ path: null,
125
+ error: `Path exceeds maximum length of ${MAX_PATH_LENGTH} characters`
126
+ };
127
+ }
128
+
129
+ // Check for shell metacharacters
130
+ if (containsShellMetacharacters(trimmed)) {
131
+ return {
132
+ valid: false,
133
+ path: null,
134
+ error: 'Path contains invalid characters'
135
+ };
136
+ }
137
+
138
+ // Check for path traversal
139
+ if (isPathTraversalAttempt(trimmed)) {
140
+ return {
141
+ valid: false,
142
+ path: null,
143
+ error: 'Path contains invalid traversal patterns'
144
+ };
145
+ }
146
+
147
+ // Normalize the path
148
+ const normalized = normalizePath(trimmed);
149
+
150
+ // Check normalized path for shell metacharacters (after expansion)
151
+ if (containsShellMetacharacters(normalized)) {
152
+ return {
153
+ valid: false,
154
+ path: null,
155
+ error: 'Resolved path contains invalid characters'
156
+ };
157
+ }
158
+
159
+ // Check if path exists
160
+ if (mustExist && !allowCreate) {
161
+ try {
162
+ const stats = fs.statSync(normalized);
163
+ if (!stats.isDirectory()) {
164
+ return {
165
+ valid: false,
166
+ path: normalized,
167
+ error: 'Path exists but is not a directory'
168
+ };
169
+ }
170
+ } catch (e) {
171
+ if (e.code === 'ENOENT') {
172
+ return {
173
+ valid: false,
174
+ path: normalized,
175
+ error: 'Directory does not exist'
176
+ };
177
+ }
178
+ return {
179
+ valid: false,
180
+ path: normalized,
181
+ error: `Cannot access directory: ${e.message}`
182
+ };
183
+ }
184
+ }
185
+
186
+ // If allowCreate, check that parent exists
187
+ if (allowCreate && mustExist) {
188
+ const parentDir = path.dirname(normalized);
189
+ try {
190
+ const parentStats = fs.statSync(parentDir);
191
+ if (!parentStats.isDirectory()) {
192
+ return {
193
+ valid: false,
194
+ path: normalized,
195
+ error: 'Parent path is not a directory'
196
+ };
197
+ }
198
+ } catch (e) {
199
+ return {
200
+ valid: false,
201
+ path: normalized,
202
+ error: 'Parent directory does not exist'
203
+ };
204
+ }
205
+ }
206
+
207
+ return {
208
+ valid: true,
209
+ path: normalized,
210
+ error: null
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Validate a file path for safe use
216
+ * @param {string} filePath - File path to validate
217
+ * @param {Object} options - Validation options
218
+ * @param {boolean} options.mustExist - If true, file must exist (default: false)
219
+ * @param {boolean} options.parentMustExist - If true, parent dir must exist (default: true)
220
+ * @returns {Object} Validation result { valid: boolean, path: string, error: string|null }
221
+ */
222
+ function validateFilePath(filePath, options = {}) {
223
+ const { mustExist = false, parentMustExist = true } = options;
224
+
225
+ // Check for null/undefined/empty
226
+ if (!filePath || typeof filePath !== 'string') {
227
+ return {
228
+ valid: false,
229
+ path: null,
230
+ error: 'File path is required'
231
+ };
232
+ }
233
+
234
+ // Trim whitespace
235
+ const trimmed = filePath.trim();
236
+ if (!trimmed) {
237
+ return {
238
+ valid: false,
239
+ path: null,
240
+ error: 'File path cannot be empty'
241
+ };
242
+ }
243
+
244
+ // Check path length
245
+ if (trimmed.length > MAX_PATH_LENGTH) {
246
+ return {
247
+ valid: false,
248
+ path: null,
249
+ error: `Path exceeds maximum length of ${MAX_PATH_LENGTH} characters`
250
+ };
251
+ }
252
+
253
+ // Check for shell metacharacters
254
+ if (containsShellMetacharacters(trimmed)) {
255
+ return {
256
+ valid: false,
257
+ path: null,
258
+ error: 'Path contains invalid characters'
259
+ };
260
+ }
261
+
262
+ // Normalize the path
263
+ const normalized = normalizePath(trimmed);
264
+
265
+ // Check if file exists when required
266
+ if (mustExist) {
267
+ try {
268
+ const stats = fs.statSync(normalized);
269
+ if (stats.isDirectory()) {
270
+ return {
271
+ valid: false,
272
+ path: normalized,
273
+ error: 'Path is a directory, not a file'
274
+ };
275
+ }
276
+ } catch (e) {
277
+ if (e.code === 'ENOENT') {
278
+ return {
279
+ valid: false,
280
+ path: normalized,
281
+ error: 'File does not exist'
282
+ };
283
+ }
284
+ return {
285
+ valid: false,
286
+ path: normalized,
287
+ error: `Cannot access file: ${e.message}`
288
+ };
289
+ }
290
+ }
291
+
292
+ // Check parent directory exists
293
+ if (parentMustExist) {
294
+ const parentDir = path.dirname(normalized);
295
+ try {
296
+ const parentStats = fs.statSync(parentDir);
297
+ if (!parentStats.isDirectory()) {
298
+ return {
299
+ valid: false,
300
+ path: normalized,
301
+ error: 'Parent path is not a directory'
302
+ };
303
+ }
304
+ } catch (e) {
305
+ return {
306
+ valid: false,
307
+ path: normalized,
308
+ error: 'Parent directory does not exist'
309
+ };
310
+ }
311
+ }
312
+
313
+ return {
314
+ valid: true,
315
+ path: normalized,
316
+ error: null
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Sanitize a string for use in a filename
322
+ * @param {string} str - String to sanitize
323
+ * @returns {string} Sanitized string
324
+ */
325
+ function sanitizeForFilename(str) {
326
+ if (!str || typeof str !== 'string') {
327
+ return '';
328
+ }
329
+ // Replace invalid characters with underscore
330
+ return str.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 200);
331
+ }
332
+
333
+ /**
334
+ * Check if a path is within a base directory (prevent escaping)
335
+ * @param {string} targetPath - Path to check
336
+ * @param {string} basePath - Base directory that should contain the path
337
+ * @returns {boolean} True if target is within base
338
+ */
339
+ function isPathWithin(targetPath, basePath) {
340
+ const normalizedTarget = normalizePath(targetPath);
341
+ const normalizedBase = normalizePath(basePath);
342
+
343
+ return normalizedTarget.startsWith(normalizedBase + path.sep) ||
344
+ normalizedTarget === normalizedBase;
345
+ }
346
+
347
+ module.exports = {
348
+ // Validation functions
349
+ validateDirectoryPath,
350
+ validateFilePath,
351
+ validateTmuxSessionName,
352
+ isValidTmuxSessionName,
353
+
354
+ // Check functions
355
+ containsShellMetacharacters,
356
+ isPathTraversalAttempt,
357
+ isPathWithin,
358
+
359
+ // Utility functions
360
+ normalizePath,
361
+ sanitizeForFilename,
362
+
363
+ // Constants
364
+ MAX_PATH_LENGTH,
365
+ SHELL_METACHARACTERS
366
+ };