i18ntk 1.10.2 → 2.0.2

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 (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1191
  3. package/main/i18ntk-analyze.js +65 -84
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +3 -3
  6. package/main/i18ntk-complete.js +90 -65
  7. package/main/i18ntk-doctor.js +123 -103
  8. package/main/i18ntk-fixer.js +61 -725
  9. package/main/i18ntk-go.js +14 -15
  10. package/main/i18ntk-init.js +77 -26
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +129 -30
  14. package/main/i18ntk-php.js +75 -75
  15. package/main/i18ntk-py.js +55 -56
  16. package/main/i18ntk-scanner.js +59 -57
  17. package/main/i18ntk-setup.js +9 -404
  18. package/main/i18ntk-sizing.js +6 -6
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +54 -18
  22. package/main/i18ntk-validate.js +13 -13
  23. package/main/manage/commands/AnalyzeCommand.js +1124 -0
  24. package/main/manage/commands/BackupCommand.js +62 -0
  25. package/main/manage/commands/CommandRouter.js +295 -0
  26. package/main/manage/commands/CompleteCommand.js +61 -0
  27. package/main/manage/commands/DoctorCommand.js +60 -0
  28. package/main/manage/commands/FixerCommand.js +624 -0
  29. package/main/manage/commands/InitCommand.js +62 -0
  30. package/main/manage/commands/ScannerCommand.js +654 -0
  31. package/main/manage/commands/SizingCommand.js +60 -0
  32. package/main/manage/commands/SummaryCommand.js +61 -0
  33. package/main/manage/commands/UsageCommand.js +60 -0
  34. package/main/manage/commands/ValidateCommand.js +978 -0
  35. package/main/manage/index-fixed.js +1447 -0
  36. package/main/manage/index.js +1462 -0
  37. package/main/manage/managers/DebugMenu.js +140 -0
  38. package/main/manage/managers/InteractiveMenu.js +177 -0
  39. package/main/manage/managers/LanguageMenu.js +62 -0
  40. package/main/manage/managers/SettingsMenu.js +53 -0
  41. package/main/manage/services/AuthenticationService.js +263 -0
  42. package/main/manage/services/ConfigurationService-fixed.js +449 -0
  43. package/main/manage/services/ConfigurationService.js +449 -0
  44. package/main/manage/services/FileManagementService.js +368 -0
  45. package/main/manage/services/FrameworkDetectionService.js +458 -0
  46. package/main/manage/services/InitService.js +1051 -0
  47. package/main/manage/services/SetupService.js +462 -0
  48. package/main/manage/services/SummaryService.js +450 -0
  49. package/main/manage/services/UsageService.js +1502 -0
  50. package/package.json +32 -29
  51. package/runtime/enhanced.d.ts +221 -221
  52. package/runtime/index.d.ts +29 -29
  53. package/runtime/index.full.d.ts +331 -331
  54. package/runtime/index.js +7 -6
  55. package/scripts/build-lite.js +17 -17
  56. package/scripts/deprecate-versions.js +23 -6
  57. package/scripts/export-translations.js +5 -5
  58. package/scripts/fix-all-i18n.js +3 -3
  59. package/scripts/fix-and-purify-i18n.js +3 -2
  60. package/scripts/fix-locale-control-chars.js +110 -0
  61. package/scripts/lint-locales.js +80 -0
  62. package/scripts/locale-optimizer.js +8 -8
  63. package/scripts/prepublish.js +21 -21
  64. package/scripts/security-check.js +117 -117
  65. package/scripts/sync-translations.js +4 -4
  66. package/scripts/sync-ui-locales.js +9 -8
  67. package/scripts/validate-all-translations.js +8 -7
  68. package/scripts/verify-deprecations.js +157 -161
  69. package/scripts/verify-translations.js +6 -5
  70. package/settings/i18ntk-config.json +282 -282
  71. package/settings/language-config.json +5 -5
  72. package/settings/settings-cli.js +9 -9
  73. package/settings/settings-manager.js +18 -18
  74. package/ui-locales/de.json +2417 -2348
  75. package/ui-locales/en.json +2415 -2352
  76. package/ui-locales/es.json +2425 -2353
  77. package/ui-locales/fr.json +2418 -2348
  78. package/ui-locales/ja.json +2463 -2361
  79. package/ui-locales/ru.json +2463 -2359
  80. package/ui-locales/zh.json +2418 -2351
  81. package/utils/admin-auth.js +2 -2
  82. package/utils/admin-cli.js +297 -297
  83. package/utils/admin-pin.js +9 -9
  84. package/utils/cli-helper.js +9 -9
  85. package/utils/config-helper.js +73 -104
  86. package/utils/config-manager.js +204 -171
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +249 -263
  89. package/utils/framework-detector.js +27 -24
  90. package/utils/i18n-helper.js +85 -41
  91. package/utils/init-helper.js +152 -94
  92. package/utils/json-output.js +98 -98
  93. package/utils/mini-commander.js +179 -0
  94. package/utils/missing-key-validator.js +5 -5
  95. package/utils/plugin-loader.js +40 -29
  96. package/utils/prompt.js +14 -44
  97. package/utils/safe-json.js +40 -0
  98. package/utils/secure-errors.js +3 -3
  99. package/utils/security-check-improved.js +390 -0
  100. package/utils/security-config.js +5 -5
  101. package/utils/security-fixed.js +607 -0
  102. package/utils/security.js +652 -602
  103. package/utils/setup-enforcer.js +136 -44
  104. package/utils/setup-validator.js +33 -32
  105. package/utils/ultra-performance-optimizer.js +11 -9
  106. package/utils/watch-locales.js +2 -1
  107. package/utils/prompt-fixed.js +0 -55
  108. package/utils/security-check.js +0 -454
package/utils/security.js CHANGED
@@ -1,602 +1,652 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const crypto = require('crypto');
4
- const configManager = require('./config-manager');
5
-
6
- // Lazy load i18n to prevent initialization race conditions
7
- let i18n;
8
- function getI18n() {
9
- if (!i18n) {
10
- try {
11
- i18n = require('./i18n-helper');
12
- } catch (error) {
13
- // Fallback to simple identity function if i18n fails to load
14
- console.warn('i18n-helper not available, using fallback messages');
15
- return { t: (key, params = {}) => key };
16
- }
17
- }
18
- return i18n;
19
- }
20
-
21
- /**
22
- * Security utility module for i18nTK
23
- * Provides secure file operations, path validation, and input sanitization
24
- * to prevent path traversal, code injection, and other security vulnerabilities
25
- */
26
- class SecurityUtils {
27
- /**
28
- * Validates and sanitizes file paths to prevent path traversal attacks
29
- * @param {string} inputPath - The input path to validate
30
- * @param {string} basePath - The base path that the input should be within (optional)
31
- * @returns {string|null} - Sanitized path or null if invalid
32
- */
33
- static validatePath(filePath, basePath = process.cwd()) {
34
- try {
35
- if (!filePath || typeof filePath !== 'string') {
36
- const i18n = getI18n();
37
- SecurityUtils.logSecurityEvent(i18n.t('security.pathValidationFailed'), 'error', { inputPath: filePath, reason: i18n.t('security.invalidInputType') });
38
- return null;
39
- }
40
-
41
- // Resolve base and target paths
42
- const base = fs.realpathSync(basePath);
43
- const resolvedPath = path.resolve(base, filePath);
44
-
45
- // Resolve symlinks if the path exists
46
- let finalPath = resolvedPath;
47
- try {
48
- finalPath = fs.realpathSync(resolvedPath);
49
- } catch {
50
- // If the path doesn't exist yet, fall back to the resolved path
51
- }
52
-
53
- // Ensure the target path is within the base directory
54
- const relativePath = path.relative(base, finalPath);
55
- if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
56
- const i18n = getI18n();
57
- SecurityUtils.logSecurityEvent(i18n.t('security.pathTraversalAttempt'), 'warning', { inputPath: filePath, resolvedPath: finalPath, basePath: base });
58
- return null;
59
- }
60
-
61
- const i18n = getI18n();
62
- SecurityUtils.logSecurityEvent(i18n.t('security.pathValidated'), 'info', { inputPath: filePath, resolvedPath: finalPath });
63
-
64
- return finalPath;
65
- } catch (error) {
66
- const i18n = getI18n();
67
- SecurityUtils.logSecurityEvent(i18n.t('security.pathValidationError'), 'error', { inputPath: filePath, error: error.message });
68
- return null;
69
- }
70
- }
71
-
72
- /**
73
- * Safely checks if a path exists.
74
- * @param {string} filePath - Path to check.
75
- * @param {string} basePath - Base path for validation.
76
- * @returns {boolean} - True if the path exists and is safe.
77
- */
78
- static safeExistsSync(filePath, basePath) {
79
- const validatedPath = this.validatePath(filePath, basePath);
80
- if (!validatedPath) {
81
- return false;
82
- }
83
- return fs.existsSync(validatedPath);
84
- }
85
-
86
- /**
87
- * Safely gets file stats.
88
- * @param {string} filePath - Path to get stats for.
89
- * @param {string} basePath - Base path for validation.
90
- * @returns {fs.Stats|null} - File stats or null if error.
91
- */
92
- static safeStatSync(filePath, basePath) {
93
- const validatedPath = this.validatePath(filePath, basePath);
94
- if (!validatedPath) {
95
- return null;
96
- }
97
- try {
98
- return fs.statSync(validatedPath);
99
- } catch (error) {
100
- return null;
101
- }
102
- }
103
-
104
- /**
105
- * Safely creates a directory.
106
- * @param {string} dirPath - Path of the directory to create.
107
- * @param {string} basePath - Base path for validation.
108
- * @param {object} options - fs.mkdirSync options.
109
- * @returns {boolean} - True if successful.
110
- */
111
- static safeMkdirSync(dirPath, basePath, options) {
112
- const validatedPath = this.validatePath(dirPath, basePath);
113
- if (!validatedPath) {
114
- return false;
115
- }
116
- try {
117
- fs.mkdirSync(validatedPath, options);
118
- return true;
119
- } catch (error) {
120
- if (error.code === 'EEXIST') {
121
- return true; // Already exists
122
- }
123
- return false;
124
- }
125
- }
126
-
127
- /**
128
- * Safely reads a file with path validation and error handling
129
- * @param {string} filePath - Path to the file
130
- * @param {string} basePath - Base path for validation
131
- * @param {string} encoding - File encoding (default: 'utf8')
132
- * @returns {Promise<string|null>} - File content or null if error
133
- */
134
- static async safeReadFile(filePath, basePath, encoding = 'utf8') {
135
- const validatedPath = this.validatePath(filePath, basePath);
136
- if (!validatedPath) {
137
- return null;
138
- }
139
-
140
- try {
141
- // Check if file exists and is readable
142
- await fs.promises.access(validatedPath, fs.constants.R_OK);
143
-
144
- // Read file with size limit (10MB max)
145
- const stats = await fs.promises.stat(validatedPath);
146
- if (stats.size > 10 * 1024 * 1024) {
147
- const i18n = getI18n();
148
- console.warn(i18n.t('security.file_too_large', { filePath: validatedPath }));
149
- return null;
150
- }
151
-
152
- return await fs.promises.readFile(validatedPath, encoding);
153
- } catch (error) {
154
- const i18n = getI18n();
155
- console.warn(i18n.t('security.file_read_error', { errorMessage: error.message }));
156
- return null;
157
- }
158
- }
159
-
160
- /**
161
- * Safely reads a file synchronously with path validation and error handling
162
- * @param {string} filePath - Path to the file
163
- * @param {string} basePath - Base path for validation
164
- * @param {string} encoding - File encoding (default: 'utf8')
165
- * @returns {string|null} - File content or null if error
166
- */
167
- static safeReadFileSync(filePath, basePath, encoding = 'utf8') {
168
- const validatedPath = this.validatePath(filePath, basePath);
169
- if (!validatedPath) {
170
- return null;
171
- }
172
- const i18n = getI18n();
173
- try {
174
- // Check if file exists and is readable
175
- fs.accessSync(validatedPath, fs.constants.R_OK);
176
-
177
- // Read file with size limit (10MB max)
178
- const stats = fs.statSync(validatedPath);
179
- if (stats.size > 10 * 1024 * 1024) {
180
- console.warn(i18n.t('security.file_too_large', { filePath: validatedPath }));
181
- return null;
182
- }
183
-
184
- return fs.readFileSync(validatedPath, encoding);
185
- } catch (error) {
186
- console.warn(i18n.t('security.file_read_error', { errorMessage: error.message }));
187
- return null;
188
- }
189
- }
190
-
191
- /**
192
- * Safely writes a file with path validation and error handling
193
- * @param {string} filePath - Path to the file
194
- * @param {string} content - Content to write
195
- * @param {string} basePath - Base path for validation
196
- * @param {string} encoding - File encoding (default: 'utf8')
197
- * @returns {Promise<boolean>} - Success status
198
- */
199
- static async safeWriteFile(filePath, content, basePath, encoding = 'utf8') {
200
- const validatedPath = this.validatePath(filePath, basePath);
201
- if (!validatedPath) {
202
- return false;
203
- }
204
-
205
- try {
206
- // Validate content size (10MB max)
207
- if (typeof content === 'string' && content.length > 10 * 1024 * 1024) {
208
- const i18n = getI18n();
209
- console.warn(i18n.t('security.content_too_large_for_file', { filePath: validatedPath }));
210
- return false;
211
- }
212
-
213
- // Ensure directory exists
214
- const dir = path.dirname(validatedPath);
215
- await fs.promises.mkdir(dir, { recursive: true });
216
-
217
- // Write file with proper permissions
218
- await fs.promises.writeFile(validatedPath, content, { encoding, mode: 0o644 });
219
- return true;
220
- } catch (error) {
221
- const i18n = getI18n();
222
- console.warn(i18n.t('security.file_write_error', { errorMessage: error.message }));
223
- return false;
224
- }
225
- }
226
-
227
- /**
228
- * Safely parses JSON with error handling and validation
229
- * @param {string} jsonString - JSON string to parse
230
- * @param {number} maxSize - Maximum allowed size (default: 1MB)
231
- * @returns {object|null} - Parsed object or null if error
232
- */
233
- static safeParseJSON(jsonString, maxSize = 1024 * 1024) {
234
- if (!jsonString || typeof jsonString !== 'string') {
235
- return null;
236
- }
237
-
238
- if (jsonString.length > maxSize) {
239
- const i18n = getI18n();
240
- console.warn(i18n.t('security.json_string_too_large'));
241
- return null;
242
- }
243
-
244
- try {
245
- return JSON.parse(jsonString);
246
- } catch (error) {
247
- const i18n = getI18n();
248
- console.warn(i18n.t('security.json_parse_error', { errorMessage: error.message }));
249
- return null;
250
- }
251
- }
252
-
253
- /**
254
- * Sanitizes user input to prevent injection attacks
255
- * @param {string} input - User input to sanitize
256
- * @param {object} options - Sanitization options
257
- * @returns {string} - Sanitized input
258
- */
259
- static sanitizeInput(input, options = {}) {
260
- if (!input || typeof input !== 'string') {
261
- return '';
262
- }
263
-
264
- const {
265
- allowedChars = /^[a-zA-Z0-9\s\-_\.\,\!\?\(\)\[\]\{\}\:\;"'\/\\]+$/,
266
- maxLength = 1000,
267
- removeHTML = true,
268
- removeScripts = true
269
- } = options;
270
-
271
- let sanitized = input.trim();
272
-
273
- // Limit length
274
- if (sanitized.length > maxLength) {
275
- sanitized = sanitized.substring(0, maxLength);
276
- }
277
-
278
- // Remove HTML tags if requested
279
- if (removeHTML) {
280
- sanitized = sanitized.replace(/<[^>]*>/g, '');
281
- }
282
-
283
- // Remove script-like content
284
- if (removeScripts) {
285
- sanitized = sanitized.replace(/javascript:/gi, '');
286
- sanitized = sanitized.replace(/on\w+\s*=/gi, '');
287
- sanitized = sanitized.replace(/eval\s*\(/gi, '');
288
- sanitized = sanitized.replace(/function\s*\(/gi, '');
289
- }
290
-
291
- // Check against allowed characters - suppress warnings for normal operations
292
- if (!allowedChars.test(sanitized)) {
293
- // Skip warning for common file path characters and reduce verbosity
294
- const isFilePath = sanitized.includes('/') || sanitized.includes('\\') || sanitized.includes('.');
295
- const isCommonContent = sanitized.length < 1000 && !sanitized.includes('<script');
296
- if (!isFilePath && !isCommonContent) {
297
- const i18n = getI18n();
298
- console.warn(i18n.t('security.inputDisallowedCharacters'));
299
- }
300
- // Allow more characters for file paths and content
301
- sanitized = sanitized.replace(/[^a-zA-Z0-9\s\-_\.\,\!\?\(\)\[\]\{\}\:\;"'\/\\]/g, '');
302
- }
303
-
304
- return sanitized;
305
- }
306
-
307
- /**
308
- * Validates command line arguments
309
- * @param {object} args - Command line arguments
310
- * @returns {object} - Validated arguments
311
- */
312
- static async validateCommandArgs(args) {
313
- const i18n = getI18n();
314
- const validatedArgs = {};
315
- const allowedArgs = [
316
- 'source-dir', 'i18n-dir', 'output-dir', 'output-report',
317
- 'help', 'language', 'strict-mode', 'exclude-files', 'no-prompt'
318
- ];
319
-
320
- for (const [key, value] of Object.entries(args)) {
321
- if (allowedArgs.includes(key)) {
322
- validatedArgs[key] = value;
323
- } else {
324
- console.warn(i18n.t('security.unknown_command_argument', { key }));
325
- }
326
- }
327
-
328
- return validatedArgs;
329
- }
330
-
331
- /**
332
- * Validates configuration object
333
- * @param {object} config - Configuration object
334
- * @returns {object|null} - Validated configuration or null if invalid
335
- */
336
- static validateConfig(config) {
337
- const i18n = getI18n();
338
- if (!config || typeof config !== 'object') {
339
- return null;
340
- }
341
-
342
- const validatedConfig = {};
343
- const allowedKeys = [
344
- 'version', 'sourceDir', 'outputDir', 'defaultLanguage', 'supportedLanguages',
345
- 'filePattern', 'excludePatterns', 'reportFormat', 'logLevel',
346
- 'i18nDir', 'sourceLanguage', 'excludeDirs', 'includeExtensions',
347
- 'translationPatterns', 'notTranslatedMarker', 'excludeFiles', 'strictMode',
348
- 'uiLanguage', 'language', 'sizeLimit', 'defaultLanguages', 'reportLanguage',
349
- 'theme', 'autoSave', 'notifications', 'dateFormat', 'timeFormat', 'timezone',
350
- 'processing', 'performance', 'advanced', 'security', 'debug', 'projectRoot', 'scriptDirectories',
351
- 'supportedExtensions', 'settings', 'backupDir', 'tempDir', 'cacheDir', 'configDir',
352
- 'displayPaths', 'reports', 'ui', 'behavior', 'dateTime', 'backup', 'framework',
353
- 'notTranslatedMarkers', 'placeholderStyles'
354
- ];
355
-
356
- const strict = config.security?.strictConfig || false;
357
-
358
- for (const [key, value] of Object.entries(config)) {
359
- if (!allowedKeys.includes(key)) {
360
- if (strict) {
361
- console.warn(i18n.t('security.unknown_config_key', { key }));
362
- }
363
- continue;
364
- }
365
-
366
- // Validate specific config values
367
- switch (key) {
368
- case 'sourceDir':
369
- case 'outputDir':
370
- if (typeof value === 'string') {
371
- // Basic path validation - will be further validated when used
372
- validatedConfig[key] = this.sanitizeInput(value, {
373
- allowedChars: /^[a-zA-Z0-9\-_\.\,\/\\:\s]+$/,
374
- maxLength: 500
375
- });
376
- }
377
- break;
378
- case 'supportedLanguages':
379
- if (Array.isArray(value)) {
380
- validatedConfig[key] = value.filter(lang =>
381
- typeof lang === 'string' && /^[a-z]{2}(-[A-Z]{2})?$/.test(lang)
382
- );
383
- }
384
- break;
385
- case 'defaultLanguage':
386
- if (typeof value === 'string' && /^[a-z]{2}(-[A-Z]{2})?$/.test(value)) {
387
- validatedConfig[key] = value;
388
- }
389
- break;
390
- default:
391
- if (typeof value === 'string') {
392
- validatedConfig[key] = this.sanitizeInput(value);
393
- } else if (typeof value === 'boolean' || typeof value === 'number') {
394
- validatedConfig[key] = value;
395
- }
396
- }
397
- }
398
-
399
- return validatedConfig;
400
- }
401
-
402
- /**
403
- * Generates a secure hash for file integrity checking
404
- * @param {string} content - Content to hash
405
- * @returns {string} - SHA-256 hash
406
- */
407
- static generateHash(content) {
408
- return crypto.createHash('sha256').update(content).digest('hex');
409
- }
410
-
411
- /**
412
- * Securely saves an encrypted PIN to the settings directory
413
- * @param {string} pin - 4 digit PIN
414
- * @returns {Promise<boolean>} - success status
415
- */
416
- static async saveEncryptedPin(pin) {
417
- try {
418
- const hash = crypto.createHash('sha256').update(pin).digest('hex');
419
- const settingsDir = require('../settings/settings-manager').configDir;
420
- const pinFile = path.join(settingsDir, 'admin-pin.hash');
421
- await fs.promises.mkdir(settingsDir, { recursive: true });
422
- await fs.promises.writeFile(pinFile, hash, 'utf8');
423
- return true;
424
- } catch (error) {
425
- return false;
426
- }
427
- }
428
-
429
- /**
430
- * Checks if a file path is safe for operations
431
- * @param {string} filePath - File path to check
432
- * @returns {boolean} - Whether the path is safe
433
- */
434
- static isSafePath(filePath) {
435
- if (!filePath || typeof filePath !== 'string') {
436
- return false;
437
- }
438
-
439
- // Check for dangerous patterns
440
- const dangerousPatterns = [
441
- /\.\./, // Parent directory traversal
442
- /^\//, // Absolute path (Unix)
443
- /^[A-Z]:\\/, // Absolute path (Windows)
444
- /~/, // Home directory
445
- /\$\{/, // Variable expansion
446
- /`/, // Command substitution
447
- /\|/, // Pipe
448
- /;/, // Command separator
449
- /&/, // Background process
450
- />/, // Redirect
451
- /</ // Redirect
452
- ];
453
-
454
- return !dangerousPatterns.some(pattern => pattern.test(filePath));
455
- }
456
-
457
- /**
458
- * Logs security events for monitoring
459
- * @param {string} event - Security event description
460
- * @param {string} level - Log level (info, warn, error)
461
- * @param {object} details - Additional details
462
- */
463
- static logSecurityEvent(event, level = 'info', details = {}) {
464
- const timestamp = new Date().toISOString();
465
- const logEntry = {
466
- timestamp,
467
- level,
468
- event,
469
- details: {
470
- ...details,
471
- pid: process.pid,
472
- nodeVersion: process.version
473
- }
474
- };
475
-
476
- // Only show security logs if debug mode is enabled and showSecurityLogs is true
477
- try {
478
- const cfg = configManager.getConfig();
479
- if (cfg.debug?.enabled && cfg.debug?.showSecurityLogs) {
480
- console.log(`[SECURITY ${level.toUpperCase()}] ${timestamp}: ${event}`, details);
481
- }
482
- } catch (error) {
483
- // Fallback: if settings can't be loaded, don't show security logs to maintain clean UI
484
- // Only log critical security events in this case
485
- if (event.includes('CRITICAL') || event.includes('BREACH') || event.includes('ATTACK')) {
486
- const i18n = getI18n();
487
- console.log(i18n.t('security.security_alert', { timestamp, event }), details);
488
- }
489
- }
490
- }
491
- /**
492
- * Safely reads directory contents with path validation
493
- * @param {string} dirPath - Directory path to read
494
- * @param {string} basePath - Base path for validation
495
- * @param {object} options - Options for readdirSync (withFileTypes, etc.)
496
- * @returns {Array|null} - Directory contents or null if error
497
- */
498
- static safeReaddirSync(dirPath, basePath, options = {}) {
499
- const validatedPath = this.validatePath(dirPath, basePath);
500
- if (!validatedPath) {
501
- return null;
502
- }
503
-
504
- try {
505
- return fs.readdirSync(validatedPath, options);
506
- } catch (error) {
507
- const i18n = getI18n();
508
- console.warn(i18n.t('security.directory_read_error', { errorMessage: error.message }));
509
- return null;
510
- }
511
- }
512
-
513
- /**
514
- * Secure performance measurement utility
515
- * Provides safe timing functionality without requiring perf_hooks
516
- * @returns {object} - Performance timing object
517
- */
518
- static getPerformanceTimer() {
519
- // Use Date.now() as a fallback if perf_hooks is not available
520
- const hasPerfHooks = (() => {
521
- try {
522
- require('perf_hooks');
523
- return true;
524
- } catch {
525
- return false;
526
- }
527
- })();
528
-
529
- if (hasPerfHooks) {
530
- const { performance } = require('perf_hooks');
531
- return {
532
- now: () => performance.now(),
533
- isHighResolution: true
534
- };
535
- } else {
536
- return {
537
- now: () => Date.now(),
538
- isHighResolution: false
539
- };
540
- }
541
- }
542
-
543
- /**
544
- * Secure debug logging utility
545
- * Provides controlled debug output based on configuration
546
- * @param {string} level - Log level (debug, info, warn, error)
547
- * @param {string} message - Log message
548
- * @param {object} details - Additional details
549
- */
550
- static debugLog(level, message, details = {}) {
551
- try {
552
- const cfg = configManager.getConfig();
553
- const debugEnabled = cfg.debug?.enabled || false;
554
- const logLevel = cfg.debug?.logLevel || 'info';
555
-
556
- if (!debugEnabled) {
557
- return;
558
- }
559
-
560
- const levels = { debug: 0, info: 1, warn: 2, error: 3 };
561
- const currentLevel = levels[logLevel] || 1;
562
- const messageLevel = levels[level] || 1;
563
-
564
- if (messageLevel < currentLevel) {
565
- return;
566
- }
567
-
568
- const timestamp = new Date().toISOString();
569
- const logEntry = {
570
- timestamp,
571
- level: level.toUpperCase(),
572
- message,
573
- details,
574
- pid: process.pid
575
- };
576
-
577
- // Only log to console if debug mode is enabled
578
- if (cfg.debug?.showConsoleOutput !== false) {
579
- console.log(`[${logEntry.level}] ${timestamp}: ${message}`, details);
580
- }
581
-
582
- // Log to file if configured
583
- if (cfg.debug?.logFile) {
584
- const fs = require('fs');
585
- const path = require('path');
586
- const logFile = path.resolve(cfg.debug.logFile);
587
-
588
- try {
589
- const logLine = JSON.stringify(logEntry) + '\n';
590
- fs.appendFileSync(logFile, logLine);
591
- } catch (error) {
592
- // Silently fail if file logging fails
593
- }
594
- }
595
- } catch (error) {
596
- // Fallback: if config can't be loaded, don't crash
597
- console.warn(`[DEBUG] ${message}`, details);
598
- }
599
- }
600
- }
601
-
602
- module.exports = SecurityUtils;
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const crypto = require('crypto');
4
+
5
+
6
+ // Lazy load configManager to avoid circular dependency
7
+ let configManager;
8
+ let configManagerLoadAttempted = false;
9
+ function getConfigManager() {
10
+ if (!configManager && !configManagerLoadAttempted) {
11
+ configManagerLoadAttempted = true;
12
+ try {
13
+ configManager = require('./config-manager');
14
+ } catch (error) {
15
+ // Return null if config-manager can't be loaded
16
+ return null;
17
+ }
18
+ }
19
+ return configManager;
20
+ }
21
+
22
+ // Lazy load i18n to prevent initialization race conditions
23
+ let i18n;
24
+ function getI18n() {
25
+ if (!i18n) {
26
+ try {
27
+ i18n = require('./i18n-helper');
28
+ } catch (error) {
29
+ // Fallback to simple identity function if i18n fails to load
30
+ console.warn('i18n-helper not available, using fallback messages');
31
+ return { t: (key, params = {}) => key };
32
+ }
33
+ }
34
+ return i18n;
35
+ }
36
+
37
+ /**
38
+ * Security utility module for i18nTK
39
+ * Provides secure file operations, path validation, and input sanitization
40
+ * to prevent path traversal, code injection, and other security vulnerabilities
41
+ */
42
+ class SecurityUtils {
43
+
44
+ // Static properties for operation tracking
45
+ static _operationStack = new Set();
46
+ static _logging = false;
47
+
48
+ constructor() {
49
+ // Instance constructor - static properties are already initialized
50
+ }
51
+
52
+ /**
53
+ * Timeout wrapper for synchronous operations to prevent hanging
54
+ * @param {Function} operation - The synchronous operation to wrap
55
+ * @param {number} timeoutMs - Timeout in milliseconds
56
+ * @param {string} operationName - Name of the operation for logging
57
+ * @returns {*} - Operation result or null if timeout/error
58
+ */
59
+ static withTimeoutSync(operation, timeoutMs = 5000, operationName = 'operation') {
60
+ // Track recursion to prevent infinite loops
61
+ if (!SecurityUtils._operationStack) {
62
+ SecurityUtils._operationStack = new Set();
63
+ }
64
+
65
+ if (SecurityUtils._operationStack.has(operationName)) {
66
+ const i18n = getI18n();
67
+ SecurityUtils.logSecurityEvent(i18n.t('security.recursion_detected', { operation: operationName }), 'error');
68
+ return null;
69
+ }
70
+
71
+ SecurityUtils._operationStack.add(operationName);
72
+
73
+ try {
74
+ // Simple timeout using setTimeout for synchronous operations
75
+ let result = null;
76
+ let hasResult = false;
77
+ let timeoutId = null;
78
+
79
+ timeoutId = setTimeout(() => {
80
+ if (!hasResult) {
81
+ const i18n = getI18n();
82
+ SecurityUtils.logSecurityEvent(i18n.t('security.operation_timeout', { operation: operationName }), 'warning');
83
+ }
84
+ }, timeoutMs);
85
+
86
+ // Execute operation synchronously
87
+ result = operation();
88
+ hasResult = true;
89
+
90
+ if (timeoutId) {
91
+ clearTimeout(timeoutId);
92
+ }
93
+
94
+ return result;
95
+ } catch (error) {
96
+ const i18n = getI18n();
97
+ console.warn(i18n.t('security.operation_error', { operation: operationName, error: error.message }));
98
+ return null;
99
+ } finally {
100
+ SecurityUtils._operationStack.delete(operationName);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Logs security events for monitoring
106
+ * @param {string} event - Security event description
107
+ * @param {string} level - Log level (info, warn, error)
108
+ * @param {object} details - Additional details
109
+ */
110
+ static logSecurityEvent(event, level = 'info', details = {}) {
111
+ // Prevent recursive logging which can occur during configuration loading
112
+ if (SecurityUtils._logging) {
113
+ return;
114
+ }
115
+
116
+ SecurityUtils._logging = true;
117
+ try {
118
+ const cfg = getConfigManager()?.getConfig?.() || {};
119
+ const envLevel = (process.env.SECURITY_LOG_LEVEL || process.env.I18NTK_SECURITY_LOG_LEVEL || '').toLowerCase();
120
+ const configLevel = (cfg.security?.logLevel || cfg.security?.audit?.logLevel || '').toLowerCase();
121
+ const currentLevel = envLevel || configLevel || 'warn';
122
+
123
+ const levels = { error: 0, warn: 1, warning: 1, info: 2 };
124
+ const messageLevel = levels[level.toLowerCase()] ?? 2;
125
+ const allowedLevel = levels[currentLevel] ?? 1;
126
+ if (messageLevel > allowedLevel) {
127
+ return;
128
+ }
129
+
130
+ const timestamp = new Date().toISOString();
131
+ const logEntry = {
132
+ timestamp,
133
+ level,
134
+ event,
135
+ details: {
136
+ ...details,
137
+ pid: process.pid,
138
+ nodeVersion: process.version
139
+ }
140
+ };
141
+
142
+ const message = `[SECURITY ${level.toUpperCase()}] ${timestamp}: ${event}`;
143
+ if (level === 'error') {
144
+ console.error(message, details);
145
+ } else if (level === 'warn' || level === 'warning') {
146
+ console.warn(message, details);
147
+ } else {
148
+ console.log(message, details);
149
+ }
150
+ } finally {
151
+ SecurityUtils._logging = false;
152
+ }
153
+ }
154
+
155
+ // Add other static methods here...
156
+ static validatePath(filePath, basePath = process.cwd(), verbose = false) {
157
+ const i18n = getI18n();
158
+ const useI18n = i18n && i18n.isInitialized && typeof i18n.t === 'function';
159
+
160
+ try {
161
+ if (!filePath || typeof filePath !== 'string') {
162
+ const message = useI18n
163
+ ? i18n.t('security.pathValidationFailed')
164
+ : 'Path validation failed';
165
+ const reason = useI18n
166
+ ? i18n.t('security.invalidInputType')
167
+ : 'Invalid input type';
168
+ SecurityUtils.logSecurityEvent(message, 'error', {
169
+ inputPath: filePath,
170
+ reason
171
+ });
172
+ return null;
173
+ }
174
+
175
+ // Check for obvious dangerous patterns first
176
+ if (!SecurityUtils.isSafePath(filePath)) {
177
+ const message = useI18n
178
+ ? i18n.t('security.pathTraversalAttempt')
179
+ : 'Path traversal attempt';
180
+ SecurityUtils.logSecurityEvent(message, 'warning', {
181
+ inputPath: filePath,
182
+ reason: 'Contains dangerous patterns'
183
+ });
184
+ return null;
185
+ }
186
+
187
+ // Resolve base and target paths
188
+ const base = fs.realpathSync(basePath);
189
+ const resolvedPath = path.resolve(base, filePath);
190
+
191
+ // Resolve symlinks if the path exists
192
+ let finalPath = resolvedPath;
193
+ try {
194
+ finalPath = fs.realpathSync(resolvedPath);
195
+ } catch {
196
+ // If the path doesn't exist yet, fall back to the resolved path
197
+ }
198
+
199
+ // Check for actual path traversal (going outside the base directory)
200
+ const relativePath = path.relative(base, finalPath);
201
+ if (relativePath.startsWith('..')) {
202
+ const message = useI18n
203
+ ? i18n.t('security.pathTraversalAttempt')
204
+ : 'Path traversal attempt';
205
+ SecurityUtils.logSecurityEvent(message, 'warning', {
206
+ inputPath: filePath,
207
+ resolvedPath: finalPath,
208
+ basePath: base,
209
+ relativePath: relativePath
210
+ });
211
+ return null;
212
+ }
213
+
214
+ // Allow absolute paths that resolve within the project structure
215
+ // The isSafePath check above already filtered out dangerous absolute paths
216
+
217
+ if (verbose) {
218
+ const successMsg = useI18n
219
+ ? i18n.t('security.pathValidated')
220
+ : 'Path validated';
221
+ SecurityUtils.logSecurityEvent(successMsg, 'info', {
222
+ inputPath: filePath,
223
+ resolvedPath: finalPath
224
+ });
225
+ }
226
+ return finalPath;
227
+ } catch (error) {
228
+ const message = useI18n
229
+ ? i18n.t('security.pathValidationError')
230
+ : 'Path validation error';
231
+ SecurityUtils.logSecurityEvent(message, 'error', {
232
+ inputPath: filePath,
233
+ error: error.message
234
+ });
235
+ return null;
236
+ }
237
+ }
238
+
239
+ static safeExistsSync(filePath, basePath, timeoutMs = 3000) {
240
+ return this.withTimeoutSync(() => {
241
+ const validatedPath = this.validatePath(filePath, basePath);
242
+ if (!validatedPath) {
243
+ return false;
244
+ }
245
+ try {
246
+ return fs.existsSync(validatedPath);
247
+ } catch (error) {
248
+ return false;
249
+ }
250
+ }, timeoutMs, 'safeExistsSync');
251
+ }
252
+
253
+ static safeReadFileSync(filePath, basePath, encoding = 'utf8') {
254
+ const validatedPath = this.validatePath(filePath, basePath);
255
+ if (!validatedPath) {
256
+ return null;
257
+ }
258
+ const i18n = getI18n();
259
+ try {
260
+ // Check if file exists and is readable
261
+ fs.accessSync(validatedPath, fs.constants.R_OK);
262
+
263
+ // Read file with size limit (10MB max)
264
+ const stats = fs.statSync(validatedPath);
265
+ if (stats.size > 10 * 1024 * 1024) {
266
+ console.warn(i18n.t('security.file_too_large', { filePath: validatedPath }));
267
+ return null;
268
+ }
269
+
270
+ return fs.readFileSync(validatedPath, encoding);
271
+ } catch (error) {
272
+ console.warn(i18n.t('security.file_read_error', { errorMessage: error.message }));
273
+ return null;
274
+ }
275
+ }
276
+
277
+ static safeWriteFileSync(filePath, content, basePath, encoding = 'utf8') {
278
+ const validatedPath = this.validatePath(filePath, basePath);
279
+ if (!validatedPath) {
280
+ return false;
281
+ }
282
+
283
+ try {
284
+ // Validate content is a string or Buffer
285
+ if (typeof content !== 'string' && !Buffer.isBuffer(content)) {
286
+ const i18n = getI18n();
287
+ console.warn(i18n.t('security.file_write_error', { errorMessage: 'Content must be a string or Buffer' }));
288
+ return false;
289
+ }
290
+
291
+ // Validate content size (10MB max)
292
+ const contentSize = typeof content === 'string' ? content.length : content.length;
293
+ if (contentSize > 10 * 1024 * 1024) {
294
+ const i18n = getI18n();
295
+ console.warn(i18n.t('security.content_too_large_for_file', { filePath: validatedPath }));
296
+ return false;
297
+ }
298
+
299
+ // Ensure directory exists
300
+ const dir = path.dirname(validatedPath);
301
+ fs.mkdirSync(dir, { recursive: true });
302
+
303
+ // Write file with proper permissions
304
+ fs.writeFileSync(validatedPath, content, { encoding, mode: 0o644 });
305
+ return true;
306
+ } catch (error) {
307
+ const i18n = getI18n();
308
+ console.warn(i18n.t('security.file_write_error', { errorMessage: error.message }));
309
+ return false;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Async compatibility wrapper for safeReadFileSync.
315
+ * @param {string} filePath
316
+ * @param {string} basePath
317
+ * @param {string} encoding
318
+ * @returns {Promise<string|null>}
319
+ */
320
+ static async safeReadFile(filePath, basePath, encoding = 'utf8') {
321
+ return this.safeReadFileSync(filePath, basePath, encoding);
322
+ }
323
+
324
+ /**
325
+ * Async compatibility wrapper for safeWriteFileSync.
326
+ * @param {string} filePath
327
+ * @param {string|Buffer} content
328
+ * @param {string} basePath
329
+ * @param {string} encoding
330
+ * @returns {Promise<boolean>}
331
+ */
332
+ static async safeWriteFile(filePath, content, basePath, encoding = 'utf8') {
333
+ return this.safeWriteFileSync(filePath, content, basePath, encoding);
334
+ }
335
+
336
+ static safeStatSync(filePath, basePath) {
337
+ const validatedPath = this.validatePath(filePath, basePath);
338
+ if (!validatedPath) {
339
+ return null;
340
+ }
341
+ try {
342
+ return fs.statSync(validatedPath);
343
+ } catch {
344
+ return null;
345
+ }
346
+ }
347
+
348
+ static safeReaddirSync(dirPath, basePath, options) {
349
+ const validatedPath = this.validatePath(dirPath, basePath);
350
+ if (!validatedPath) {
351
+ return [];
352
+ }
353
+ try {
354
+ return fs.readdirSync(validatedPath, options);
355
+ } catch {
356
+ return [];
357
+ }
358
+ }
359
+
360
+ static safeMkdirSync(dirPath, basePath, options = { recursive: true }) {
361
+ const validatedPath = this.validatePath(dirPath, basePath);
362
+ if (!validatedPath) {
363
+ return false;
364
+ }
365
+ try {
366
+ fs.mkdirSync(validatedPath, options);
367
+ return true;
368
+ } catch {
369
+ return false;
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Safely parse JSON content.
375
+ * Accepts both raw JSON strings and already-parsed objects.
376
+ * @param {string|object} input - JSON string or object
377
+ * @param {*} fallback - Value to return on parse error
378
+ * @returns {*}
379
+ */
380
+ static safeParseJSON(input, fallback = null) {
381
+ if (input === null || input === undefined) {
382
+ return fallback;
383
+ }
384
+
385
+ if (typeof input === 'object') {
386
+ return input;
387
+ }
388
+
389
+ if (typeof input !== 'string') {
390
+ return fallback;
391
+ }
392
+
393
+ const trimmed = input.trim();
394
+ if (!trimmed) {
395
+ return fallback;
396
+ }
397
+
398
+ try {
399
+ const normalized = trimmed.charCodeAt(0) === 0xFEFF ? trimmed.slice(1) : trimmed;
400
+ return JSON.parse(normalized);
401
+ } catch (error) {
402
+ console.warn(`Invalid JSON content: ${error.message}`);
403
+ return fallback;
404
+ }
405
+ }
406
+
407
+ static sanitizeInput(input, options = {}) {
408
+ if (!input || typeof input !== 'string') {
409
+ return '';
410
+ }
411
+
412
+ const {
413
+ allowedChars = /^[a-zA-Z0-9\s\-_\.\,\!\?\(\)\[\]\{\}\:\;"'\/\\]+$/,
414
+ maxLength = 1000,
415
+ removeHTML = true,
416
+ removeScripts = true
417
+ } = options;
418
+
419
+ let sanitized = input.trim();
420
+
421
+ // Limit length
422
+ if (sanitized.length > maxLength) {
423
+ sanitized = sanitized.substring(0, maxLength);
424
+ }
425
+
426
+ // Remove HTML tags if requested
427
+ if (removeHTML) {
428
+ sanitized = sanitized.replace(/<[^>]*>/g, '');
429
+ }
430
+
431
+ // Remove script-like content
432
+ if (removeScripts) {
433
+ sanitized = sanitized.replace(/javascript:/gi, '');
434
+ sanitized = sanitized.replace(/on\w+\s*=/gi, '');
435
+ sanitized = sanitized.replace(/eval\s*\(/gi, '');
436
+ sanitized = sanitized.replace(/function\s*\(/gi, '');
437
+ }
438
+
439
+ // Check against allowed characters - suppress warnings for normal operations
440
+ if (!allowedChars.test(sanitized)) {
441
+ // Skip warning for common file path characters and reduce verbosity
442
+ const isFilePath = sanitized.includes('/') || sanitized.includes('\\') || sanitized.includes('.');
443
+ const isCommonContent = sanitized.length < 1000 && !sanitized.includes('<script');
444
+ if (!isFilePath && !isCommonContent) {
445
+ const i18n = getI18n();
446
+ console.warn(i18n.t('security.inputDisallowedCharacters'));
447
+ }
448
+ // Allow more characters for file paths and content
449
+ sanitized = sanitized.replace(/[^a-zA-Z0-9\s\-_\.\,\!\?\(\)\[\]\{\}\:\;"'\/\\]/g, '');
450
+ }
451
+
452
+ return sanitized;
453
+ }
454
+
455
+ static generateHash(content) {
456
+ return crypto.createHash('sha256').update(content).digest('hex');
457
+ }
458
+
459
+ static isSafePath(filePath) {
460
+ if (!filePath || typeof filePath !== 'string') {
461
+ return false;
462
+ }
463
+
464
+ // Allow legitimate Windows drive letter paths
465
+ if (filePath.match(/^[A-Z]:[\/\\]/)) {
466
+ const afterDrive = filePath.substring(3);
467
+ // Only check the part after the drive letter for dangerous patterns
468
+ const dangerousPatterns = [
469
+ /\.\./, // Parent directory traversal
470
+ /~/, // Home directory
471
+ /\$\{/, // Variable expansion
472
+ /`/, // Command substitution
473
+ /\|/, // Pipe
474
+ /;/, // Command separator
475
+ /&/, // Background process
476
+ />/, // Redirect
477
+ /</ // Redirect
478
+ ];
479
+ return !dangerousPatterns.some(pattern => pattern.test(afterDrive));
480
+ }
481
+
482
+ // Check for dangerous patterns in non-Windows paths
483
+ const dangerousPatterns = [
484
+ /\.\./, // Parent directory traversal
485
+ /^\//, // Absolute path (Unix) - but allow for legitimate use
486
+ /~/, // Home directory
487
+ /\$\{/, // Variable expansion
488
+ /`/, // Command substitution
489
+ /\|/, // Pipe
490
+ /;/, // Command separator
491
+ /&/, // Background process
492
+ />/, // Redirect
493
+ /</ // Redirect
494
+ ];
495
+
496
+ // Allow absolute paths that are within the project structure
497
+ if (filePath.startsWith('/') || filePath.startsWith('\\')) {
498
+ // Allow absolute paths but check for dangerous patterns
499
+ const hasDangerousPatterns = dangerousPatterns.slice(1).some(pattern => pattern.test(filePath));
500
+ return !hasDangerousPatterns;
501
+ }
502
+
503
+ return !dangerousPatterns.some(pattern => pattern.test(filePath));
504
+ }
505
+ static validateConfig(config) {
506
+ if (!config || typeof config !== 'object') {
507
+ SecurityUtils.logSecurityEvent('Invalid configuration object provided', 'error', {
508
+ configType: typeof config
509
+ });
510
+ return {};
511
+ }
512
+
513
+ const sanitized = { ...config };
514
+ const i18n = getI18n();
515
+
516
+ // Define allowed configuration properties
517
+ const allowedProperties = new Set([
518
+ // Core directories and paths
519
+ 'projectRoot', 'sourceDir', 'i18nDir', 'outputDir', 'backupDir', 'tempDir', 'cacheDir', 'configDir',
520
+ // Language settings
521
+ 'sourceLanguage', 'uiLanguage', 'language', 'defaultLanguages', 'supportedLanguages',
522
+ // Translation markers and content
523
+ 'notTranslatedMarker', 'notTranslatedMarkers', 'translatedMarker', 'translatedMarkers',
524
+ // File handling
525
+ 'supportedExtensions', 'excludeFiles', 'excludeDirs', 'includeFiles', 'includeDirs',
526
+ // Operational settings
527
+ 'strictMode', 'debug', 'displayPaths', 'version', 'scriptDirectories',
528
+ // Framework and processing
529
+ 'framework', 'processing', 'performance', 'advanced',
530
+ // UI and theme settings
531
+ 'theme', 'ui', 'setup', 'reports', 'display', 'interface',
532
+ // Security and settings
533
+ 'security', 'settings', 'preferences', 'config', 'configuration',
534
+ // Additional common properties
535
+ 'autoSave', 'autoBackup', 'validateOnSave', 'showWarnings', 'verbose',
536
+ 'timeout', 'retries', 'batchSize', 'maxConcurrency', 'cacheEnabled'
537
+ ]);
538
+
539
+ // Remove unknown properties
540
+ Object.keys(sanitized).forEach(key => {
541
+ if (!allowedProperties.has(key)) {
542
+ // Only log warnings for properties that might be security risks
543
+ const value = sanitized[key];
544
+ const isSuspicious = typeof value === 'string' &&
545
+ (value.includes('..') || value.includes('/') || value.includes('\\') ||
546
+ value.includes('$') || value.includes('`') || value.includes('|') ||
547
+ value.includes(';') || value.includes('&'));
548
+
549
+ if (isSuspicious) {
550
+ SecurityUtils.logSecurityEvent('Removing potentially suspicious configuration property', 'warn', {
551
+ property: key,
552
+ value: sanitized[key]
553
+ });
554
+ } else {
555
+ // Use info level for normal unknown properties to reduce noise
556
+ SecurityUtils.logSecurityEvent('Removing unknown configuration property', 'info', {
557
+ property: key,
558
+ value: sanitized[key]
559
+ });
560
+ }
561
+ delete sanitized[key];
562
+ }
563
+ });
564
+
565
+ // Validate and sanitize path properties
566
+ const pathProperties = ['projectRoot', 'sourceDir', 'i18nDir', 'outputDir', 'backupDir', 'tempDir', 'cacheDir', 'configDir'];
567
+
568
+ pathProperties.forEach(prop => {
569
+ if (sanitized[prop] && typeof sanitized[prop] === 'string') {
570
+ let originalPath = sanitized[prop];
571
+
572
+ // Skip validation for legitimate absolute paths
573
+ const isWindowsAbsolute = originalPath.match(/^[A-Z]:[\/\\]/i);
574
+ const isUnixAbsolute = originalPath.startsWith('/') || originalPath.startsWith('\\');
575
+
576
+ if (isWindowsAbsolute || isUnixAbsolute) {
577
+ // Allow absolute paths - they're legitimate for project directories
578
+ return;
579
+ }
580
+
581
+ // Only validate relative paths for dangerous patterns
582
+ if (!SecurityUtils.isSafePath(originalPath)) {
583
+ SecurityUtils.logSecurityEvent('Path validation failed for configuration property', 'warn', {
584
+ property: prop,
585
+ originalPath: originalPath
586
+ });
587
+
588
+ // For relative paths, ensure they're safe
589
+ let sanitizedPath = originalPath.replace(/\.\.[\/\\]/g, '');
590
+ sanitizedPath = sanitizedPath.replace(/[|;&$`{}()[\]<>?]/g, '');
591
+
592
+ if (sanitizedPath !== originalPath) {
593
+ SecurityUtils.logSecurityEvent('Path sanitized for configuration property', 'info', {
594
+ property: prop,
595
+ originalPath: originalPath,
596
+ sanitizedPath: sanitizedPath
597
+ });
598
+ sanitized[prop] = sanitizedPath;
599
+ }
600
+ }
601
+ }
602
+ });
603
+
604
+ // Validate security settings
605
+ if (sanitized.security) {
606
+ const security = sanitized.security;
607
+
608
+ // Validate session timeout (should be reasonable)
609
+ if (security.sessionTimeout && (typeof security.sessionTimeout !== 'number' || security.sessionTimeout < 60000 || security.sessionTimeout > 86400000)) {
610
+ SecurityUtils.logSecurityEvent('Invalid session timeout in security configuration', 'warn', {
611
+ sessionTimeout: security.sessionTimeout
612
+ });
613
+ security.sessionTimeout = 1800000; // Default to 30 minutes
614
+ }
615
+
616
+ // Validate max failed attempts
617
+ if (security.maxFailedAttempts && (typeof security.maxFailedAttempts !== 'number' || security.maxFailedAttempts < 1 || security.maxFailedAttempts > 10)) {
618
+ SecurityUtils.logSecurityEvent('Invalid max failed attempts in security configuration', 'warn', {
619
+ maxFailedAttempts: security.maxFailedAttempts
620
+ });
621
+ security.maxFailedAttempts = 3; // Default to 3 attempts
622
+ }
623
+ }
624
+
625
+ // Validate language settings
626
+ if (sanitized.sourceLanguage && typeof sanitized.sourceLanguage === 'string') {
627
+ // Sanitize language code (only allow alphanumeric, hyphens, underscores)
628
+ sanitized.sourceLanguage = sanitized.sourceLanguage.replace(/[^a-zA-Z0-9\-_]/g, '');
629
+ }
630
+
631
+ if (sanitized.uiLanguage && typeof sanitized.uiLanguage === 'string') {
632
+ sanitized.uiLanguage = sanitized.uiLanguage.replace(/[^a-zA-Z0-9\-_]/g, '');
633
+ }
634
+
635
+ // Validate default languages array
636
+ if (sanitized.defaultLanguages && Array.isArray(sanitized.defaultLanguages)) {
637
+ sanitized.defaultLanguages = sanitized.defaultLanguages
638
+ .filter(lang => typeof lang === 'string')
639
+ .map(lang => lang.replace(/[^a-zA-Z0-9\-_]/g, ''))
640
+ .filter(lang => lang.length > 0);
641
+ }
642
+
643
+ SecurityUtils.logSecurityEvent('Configuration validation completed', 'info', {
644
+ propertiesCount: Object.keys(sanitized).length,
645
+ sanitizedPaths: pathProperties.filter(prop => sanitized[prop]).length
646
+ });
647
+
648
+ return sanitized;
649
+ }
650
+ }
651
+
652
+ module.exports = SecurityUtils;