i18ntk 2.0.4 → 2.2.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/README.md +38 -60
- package/main/i18ntk-analyze.js +49 -44
- package/main/i18ntk-complete.js +75 -74
- package/main/i18ntk-fixer.js +3 -3
- package/main/i18ntk-init.js +5 -5
- package/main/i18ntk-scanner.js +2 -2
- package/main/i18ntk-sizing.js +35 -35
- package/main/i18ntk-summary.js +4 -4
- package/main/i18ntk-ui.js +54 -8
- package/main/i18ntk-usage.js +14 -14
- package/main/i18ntk-validate.js +6 -5
- package/main/manage/commands/AnalyzeCommand.js +40 -35
- package/main/manage/commands/FixerCommand.js +2 -2
- package/main/manage/commands/ScannerCommand.js +2 -2
- package/main/manage/commands/ValidateCommand.js +9 -9
- package/main/manage/index.js +147 -75
- package/main/manage/managers/LanguageMenu.js +7 -2
- package/main/manage/services/UsageService.js +7 -7
- package/package.json +269 -290
- package/settings/settings-cli.js +3 -3
- package/ui-locales/de.json +161 -166
- package/ui-locales/en.json +13 -18
- package/ui-locales/es.json +171 -184
- package/ui-locales/fr.json +155 -161
- package/ui-locales/ja.json +192 -243
- package/ui-locales/ru.json +145 -196
- package/ui-locales/zh.json +179 -185
- package/utils/cli-helper.js +26 -98
- package/utils/extractors/regex.js +39 -12
- package/utils/i18n-helper.js +88 -40
- package/{scripts → utils}/locale-optimizer.js +61 -60
- package/utils/security-check-improved.js +16 -13
- package/utils/security.js +6 -4
- package/main/i18ntk-go.js +0 -283
- package/main/i18ntk-java.js +0 -380
- package/main/i18ntk-js.js +0 -512
- package/main/i18ntk-manage.js +0 -1694
- package/main/i18ntk-php.js +0 -462
- package/main/i18ntk-py.js +0 -379
- package/main/i18ntk-settings.js +0 -23
- package/main/manage/index-fixed.js +0 -1447
- package/main/manage/services/ConfigurationService-fixed.js +0 -449
- package/scripts/build-lite.js +0 -279
- package/scripts/deprecate-versions.js +0 -317
- package/scripts/export-translations.js +0 -84
- package/scripts/fix-all-i18n.js +0 -215
- package/scripts/fix-and-purify-i18n.js +0 -213
- package/scripts/fix-locale-control-chars.js +0 -110
- package/scripts/lint-locales.js +0 -80
- package/scripts/prepublish.js +0 -348
- package/scripts/security-check.js +0 -117
- package/scripts/sync-translations.js +0 -151
- package/scripts/sync-ui-locales.js +0 -20
- package/scripts/validate-all-translations.js +0 -139
- package/scripts/verify-deprecations.js +0 -157
- package/scripts/verify-translations.js +0 -63
- package/utils/security-fixed.js +0 -607
package/utils/security-fixed.js
DELETED
|
@@ -1,607 +0,0 @@
|
|
|
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
|
-
function getConfigManager() {
|
|
9
|
-
if (!configManager) {
|
|
10
|
-
try {
|
|
11
|
-
configManager = require('./config-manager');
|
|
12
|
-
} catch (error) {
|
|
13
|
-
// Return null if config-manager can't be loaded
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return configManager;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Lazy load i18n to prevent initialization race conditions
|
|
21
|
-
let i18n;
|
|
22
|
-
function getI18n() {
|
|
23
|
-
if (!i18n) {
|
|
24
|
-
try {
|
|
25
|
-
i18n = require('./i18n-helper');
|
|
26
|
-
} catch (error) {
|
|
27
|
-
// Fallback to simple identity function if i18n fails to load
|
|
28
|
-
console.warn('i18n-helper not available, using fallback messages');
|
|
29
|
-
return { t: (key, params = {}) => key };
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return i18n;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Security utility module for i18nTK
|
|
37
|
-
* Provides secure file operations, path validation, and input sanitization
|
|
38
|
-
* to prevent path traversal, code injection, and other security vulnerabilities
|
|
39
|
-
*/
|
|
40
|
-
console.log('🔍 DEBUG: SecurityUtils class loaded successfully');
|
|
41
|
-
class SecurityUtils {
|
|
42
|
-
|
|
43
|
-
// Static properties for operation tracking
|
|
44
|
-
static _operationStack = new Set();
|
|
45
|
-
static _logging = false;
|
|
46
|
-
|
|
47
|
-
constructor() {
|
|
48
|
-
// Instance constructor - static properties are already initialized
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Timeout wrapper for synchronous operations to prevent hanging
|
|
53
|
-
* @param {Function} operation - The synchronous operation to wrap
|
|
54
|
-
* @param {number} timeoutMs - Timeout in milliseconds
|
|
55
|
-
* @param {string} operationName - Name of the operation for logging
|
|
56
|
-
* @returns {*} - Operation result or null if timeout/error
|
|
57
|
-
*/
|
|
58
|
-
static withTimeoutSync(operation, timeoutMs = 5000, operationName = 'operation') {
|
|
59
|
-
// Track recursion to prevent infinite loops
|
|
60
|
-
if (!SecurityUtils._operationStack) {
|
|
61
|
-
SecurityUtils._operationStack = new Set();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (SecurityUtils._operationStack.has(operationName)) {
|
|
65
|
-
const i18n = getI18n();
|
|
66
|
-
SecurityUtils.logSecurityEvent(i18n.t('security.recursion_detected', { operation: operationName }), 'error');
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
SecurityUtils._operationStack.add(operationName);
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
// Simple timeout using setTimeout for synchronous operations
|
|
74
|
-
let result = null;
|
|
75
|
-
let hasResult = false;
|
|
76
|
-
let timeoutId = null;
|
|
77
|
-
|
|
78
|
-
const timeoutPromise = new Promise((resolve) => {
|
|
79
|
-
timeoutId = setTimeout(() => {
|
|
80
|
-
if (!hasResult) {
|
|
81
|
-
const i18n = getI18n();
|
|
82
|
-
SecurityUtils.logSecurityEvent(i18n.t('security.operation_timeout', { operation: operationName }), 'warning');
|
|
83
|
-
resolve(null);
|
|
84
|
-
}
|
|
85
|
-
}, timeoutMs);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Execute operation synchronously
|
|
89
|
-
result = operation();
|
|
90
|
-
hasResult = true;
|
|
91
|
-
|
|
92
|
-
if (timeoutId) {
|
|
93
|
-
clearTimeout(timeoutId);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return result;
|
|
97
|
-
} catch (error) {
|
|
98
|
-
const i18n = getI18n();
|
|
99
|
-
console.warn(i18n.t('security.operation_error', { operation: operationName, error: error.message }));
|
|
100
|
-
return null;
|
|
101
|
-
} finally {
|
|
102
|
-
SecurityUtils._operationStack.delete(operationName);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Logs security events for monitoring
|
|
108
|
-
* @param {string} event - Security event description
|
|
109
|
-
* @param {string} level - Log level (info, warn, error)
|
|
110
|
-
* @param {object} details - Additional details
|
|
111
|
-
*/
|
|
112
|
-
static logSecurityEvent(event, level = 'info', details = {}) {
|
|
113
|
-
// Prevent recursive logging which can occur during configuration loading
|
|
114
|
-
if (SecurityUtils._logging) {
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
SecurityUtils._logging = true;
|
|
119
|
-
try {
|
|
120
|
-
const cfg = getConfigManager()?.getConfig?.() || {};
|
|
121
|
-
const envLevel = (process.env.SECURITY_LOG_LEVEL || process.env.I18NTK_SECURITY_LOG_LEVEL || '').toLowerCase();
|
|
122
|
-
const configLevel = (cfg.security?.logLevel || cfg.security?.audit?.logLevel || '').toLowerCase();
|
|
123
|
-
const currentLevel = envLevel || configLevel || 'warn';
|
|
124
|
-
|
|
125
|
-
const levels = { error: 0, warn: 1, warning: 1, info: 2 };
|
|
126
|
-
const messageLevel = levels[level.toLowerCase()] ?? 2;
|
|
127
|
-
const allowedLevel = levels[currentLevel] ?? 1;
|
|
128
|
-
if (messageLevel > allowedLevel) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const timestamp = new Date().toISOString();
|
|
133
|
-
const logEntry = {
|
|
134
|
-
timestamp,
|
|
135
|
-
level,
|
|
136
|
-
event,
|
|
137
|
-
details: {
|
|
138
|
-
...details,
|
|
139
|
-
pid: process.pid,
|
|
140
|
-
nodeVersion: process.version
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const message = `[SECURITY ${level.toUpperCase()}] ${timestamp}: ${event}`;
|
|
145
|
-
if (level === 'error') {
|
|
146
|
-
console.error(message, details);
|
|
147
|
-
} else if (level === 'warn' || level === 'warning') {
|
|
148
|
-
console.warn(message, details);
|
|
149
|
-
} else {
|
|
150
|
-
console.log(message, details);
|
|
151
|
-
}
|
|
152
|
-
} finally {
|
|
153
|
-
SecurityUtils._logging = false;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Add other static methods here...
|
|
158
|
-
static validatePath(filePath, basePath = process.cwd(), verbose = false) {
|
|
159
|
-
const i18n = getI18n();
|
|
160
|
-
const useI18n = i18n && i18n.isInitialized && typeof i18n.t === 'function';
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
if (!filePath || typeof filePath !== 'string') {
|
|
164
|
-
const message = useI18n
|
|
165
|
-
? i18n.t('security.pathValidationFailed')
|
|
166
|
-
: 'Path validation failed';
|
|
167
|
-
const reason = useI18n
|
|
168
|
-
? i18n.t('security.invalidInputType')
|
|
169
|
-
: 'Invalid input type';
|
|
170
|
-
SecurityUtils.logSecurityEvent(message, 'error', {
|
|
171
|
-
inputPath: filePath,
|
|
172
|
-
reason
|
|
173
|
-
});
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Check for obvious dangerous patterns first
|
|
178
|
-
if (!SecurityUtils.isSafePath(filePath)) {
|
|
179
|
-
const message = useI18n
|
|
180
|
-
? i18n.t('security.pathTraversalAttempt')
|
|
181
|
-
: 'Path traversal attempt';
|
|
182
|
-
SecurityUtils.logSecurityEvent(message, 'warning', {
|
|
183
|
-
inputPath: filePath,
|
|
184
|
-
reason: 'Contains dangerous patterns'
|
|
185
|
-
});
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Resolve base and target paths
|
|
190
|
-
const base = fs.realpathSync(basePath);
|
|
191
|
-
const resolvedPath = path.resolve(base, filePath);
|
|
192
|
-
|
|
193
|
-
// Resolve symlinks if the path exists
|
|
194
|
-
let finalPath = resolvedPath;
|
|
195
|
-
try {
|
|
196
|
-
finalPath = fs.realpathSync(resolvedPath);
|
|
197
|
-
} catch {
|
|
198
|
-
// If the path doesn't exist yet, fall back to the resolved path
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Check for actual path traversal (going outside the base directory)
|
|
202
|
-
const relativePath = path.relative(base, finalPath);
|
|
203
|
-
if (relativePath.startsWith('..')) {
|
|
204
|
-
const message = useI18n
|
|
205
|
-
? i18n.t('security.pathTraversalAttempt')
|
|
206
|
-
: 'Path traversal attempt';
|
|
207
|
-
SecurityUtils.logSecurityEvent(message, 'warning', {
|
|
208
|
-
inputPath: filePath,
|
|
209
|
-
resolvedPath: finalPath,
|
|
210
|
-
basePath: base,
|
|
211
|
-
relativePath: relativePath
|
|
212
|
-
});
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Allow absolute paths that resolve within the project structure
|
|
217
|
-
// The isSafePath check above already filtered out dangerous absolute paths
|
|
218
|
-
|
|
219
|
-
if (verbose) {
|
|
220
|
-
const successMsg = useI18n
|
|
221
|
-
? i18n.t('security.pathValidated')
|
|
222
|
-
: 'Path validated';
|
|
223
|
-
SecurityUtils.logSecurityEvent(successMsg, 'info', {
|
|
224
|
-
inputPath: filePath,
|
|
225
|
-
resolvedPath: finalPath
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
return finalPath;
|
|
229
|
-
} catch (error) {
|
|
230
|
-
const message = useI18n
|
|
231
|
-
? i18n.t('security.pathValidationError')
|
|
232
|
-
: 'Path validation error';
|
|
233
|
-
SecurityUtils.logSecurityEvent(message, 'error', {
|
|
234
|
-
inputPath: filePath,
|
|
235
|
-
error: error.message
|
|
236
|
-
});
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
static safeExistsSync(filePath, basePath, timeoutMs = 3000) {
|
|
242
|
-
return this.withTimeoutSync(() => {
|
|
243
|
-
const validatedPath = this.validatePath(filePath, basePath);
|
|
244
|
-
if (!validatedPath) {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
try {
|
|
248
|
-
return fs.existsSync(validatedPath);
|
|
249
|
-
} catch (error) {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
}, timeoutMs, 'safeExistsSync');
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
static safeReadFileSync(filePath, basePath, encoding = 'utf8') {
|
|
256
|
-
const validatedPath = this.validatePath(filePath, basePath);
|
|
257
|
-
if (!validatedPath) {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
const i18n = getI18n();
|
|
261
|
-
try {
|
|
262
|
-
// Check if file exists and is readable
|
|
263
|
-
fs.accessSync(validatedPath, fs.constants.R_OK);
|
|
264
|
-
|
|
265
|
-
// Read file with size limit (10MB max)
|
|
266
|
-
const stats = fs.statSync(validatedPath);
|
|
267
|
-
if (stats.size > 10 * 1024 * 1024) {
|
|
268
|
-
console.warn(i18n.t('security.file_too_large', { filePath: validatedPath }));
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return fs.readFileSync(validatedPath, encoding);
|
|
273
|
-
} catch (error) {
|
|
274
|
-
console.warn(i18n.t('security.file_read_error', { errorMessage: error.message }));
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
static safeWriteFileSync(filePath, content, basePath, encoding = 'utf8') {
|
|
280
|
-
const validatedPath = this.validatePath(filePath, basePath);
|
|
281
|
-
if (!validatedPath) {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
// Validate content size (10MB max)
|
|
287
|
-
if (typeof content === 'string' && content.length > 10 * 1024 * 1024) {
|
|
288
|
-
const i18n = getI18n();
|
|
289
|
-
console.warn(i18n.t('security.content_too_large_for_file', { filePath: validatedPath }));
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Ensure directory exists
|
|
294
|
-
const dir = path.dirname(validatedPath);
|
|
295
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
296
|
-
|
|
297
|
-
// Write file with proper permissions
|
|
298
|
-
fs.writeFileSync(validatedPath, content, { encoding, mode: 0o644 });
|
|
299
|
-
return true;
|
|
300
|
-
} catch (error) {
|
|
301
|
-
const i18n = getI18n();
|
|
302
|
-
console.warn(i18n.t('security.file_write_error', { errorMessage: error.message }));
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Async compatibility wrapper for safeReadFileSync.
|
|
309
|
-
* @param {string} filePath
|
|
310
|
-
* @param {string} basePath
|
|
311
|
-
* @param {string} encoding
|
|
312
|
-
* @returns {Promise<string|null>}
|
|
313
|
-
*/
|
|
314
|
-
static async safeReadFile(filePath, basePath, encoding = 'utf8') {
|
|
315
|
-
return this.safeReadFileSync(filePath, basePath, encoding);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Async compatibility wrapper for safeWriteFileSync.
|
|
320
|
-
* @param {string} filePath
|
|
321
|
-
* @param {string|Buffer} content
|
|
322
|
-
* @param {string} basePath
|
|
323
|
-
* @param {string} encoding
|
|
324
|
-
* @returns {Promise<boolean>}
|
|
325
|
-
*/
|
|
326
|
-
static async safeWriteFile(filePath, content, basePath, encoding = 'utf8') {
|
|
327
|
-
return this.safeWriteFileSync(filePath, content, basePath, encoding);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Safely parse JSON content.
|
|
332
|
-
* Accepts both raw JSON strings and already-parsed objects.
|
|
333
|
-
* @param {string|object} input - JSON string or object
|
|
334
|
-
* @param {*} fallback - Value to return on parse error
|
|
335
|
-
* @returns {*}
|
|
336
|
-
*/
|
|
337
|
-
static safeParseJSON(input, fallback = null) {
|
|
338
|
-
if (input === null || input === undefined) {
|
|
339
|
-
return fallback;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (typeof input === 'object') {
|
|
343
|
-
return input;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (typeof input !== 'string') {
|
|
347
|
-
return fallback;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const trimmed = input.trim();
|
|
351
|
-
if (!trimmed) {
|
|
352
|
-
return fallback;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
try {
|
|
356
|
-
const normalized = trimmed.charCodeAt(0) === 0xFEFF ? trimmed.slice(1) : trimmed;
|
|
357
|
-
return JSON.parse(normalized);
|
|
358
|
-
} catch (error) {
|
|
359
|
-
console.warn(`Invalid JSON content: ${error.message}`);
|
|
360
|
-
return fallback;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
static sanitizeInput(input, options = {}) {
|
|
365
|
-
if (!input || typeof input !== 'string') {
|
|
366
|
-
return '';
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const {
|
|
370
|
-
allowedChars = /^[a-zA-Z0-9\s\-_\.\,\!\?\(\)\[\]\{\}\:\;"'\/\\]+$/,
|
|
371
|
-
maxLength = 1000,
|
|
372
|
-
removeHTML = true,
|
|
373
|
-
removeScripts = true
|
|
374
|
-
} = options;
|
|
375
|
-
|
|
376
|
-
let sanitized = input.trim();
|
|
377
|
-
|
|
378
|
-
// Limit length
|
|
379
|
-
if (sanitized.length > maxLength) {
|
|
380
|
-
sanitized = sanitized.substring(0, maxLength);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Remove HTML tags if requested
|
|
384
|
-
if (removeHTML) {
|
|
385
|
-
sanitized = sanitized.replace(/<[^>]*>/g, '');
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Remove script-like content
|
|
389
|
-
if (removeScripts) {
|
|
390
|
-
sanitized = sanitized.replace(/javascript:/gi, '');
|
|
391
|
-
sanitized = sanitized.replace(/on\w+\s*=/gi, '');
|
|
392
|
-
sanitized = sanitized.replace(/eval\s*\(/gi, '');
|
|
393
|
-
sanitized = sanitized.replace(/function\s*\(/gi, '');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Check against allowed characters - suppress warnings for normal operations
|
|
397
|
-
if (!allowedChars.test(sanitized)) {
|
|
398
|
-
// Skip warning for common file path characters and reduce verbosity
|
|
399
|
-
const isFilePath = sanitized.includes('/') || sanitized.includes('\\') || sanitized.includes('.');
|
|
400
|
-
const isCommonContent = sanitized.length < 1000 && !sanitized.includes('<script');
|
|
401
|
-
if (!isFilePath && !isCommonContent) {
|
|
402
|
-
const i18n = getI18n();
|
|
403
|
-
console.warn(i18n.t('security.inputDisallowedCharacters'));
|
|
404
|
-
}
|
|
405
|
-
// Allow more characters for file paths and content
|
|
406
|
-
sanitized = sanitized.replace(/[^a-zA-Z0-9\s\-_\.\,\!\?\(\)\[\]\{\}\:\;"'\/\\]/g, '');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return sanitized;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
static generateHash(content) {
|
|
413
|
-
return crypto.createHash('sha256').update(content).digest('hex');
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
static isSafePath(filePath) {
|
|
417
|
-
if (!filePath || typeof filePath !== 'string') {
|
|
418
|
-
return false;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Check for dangerous patterns
|
|
422
|
-
const dangerousPatterns = [
|
|
423
|
-
/\.\./, // Parent directory traversal
|
|
424
|
-
/^\//, // Absolute path (Unix)
|
|
425
|
-
/^[A-Z]:\\/, // Absolute path (Windows)
|
|
426
|
-
/~/, // Home directory
|
|
427
|
-
/\$\{/, // Variable expansion
|
|
428
|
-
/`/, // Command substitution
|
|
429
|
-
/\|/, // Pipe
|
|
430
|
-
/;/, // Command separator
|
|
431
|
-
/&/, // Background process
|
|
432
|
-
/>/, // Redirect
|
|
433
|
-
/</ // Redirect
|
|
434
|
-
];
|
|
435
|
-
|
|
436
|
-
return !dangerousPatterns.some(pattern => pattern.test(filePath));
|
|
437
|
-
}
|
|
438
|
-
static validateConfig(config) {
|
|
439
|
-
if (!config || typeof config !== 'object') {
|
|
440
|
-
SecurityUtils.logSecurityEvent('Invalid configuration object provided', 'error', {
|
|
441
|
-
configType: typeof config
|
|
442
|
-
});
|
|
443
|
-
return {};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const sanitized = { ...config };
|
|
447
|
-
const i18n = getI18n();
|
|
448
|
-
|
|
449
|
-
// Define allowed configuration properties
|
|
450
|
-
const allowedProperties = new Set([
|
|
451
|
-
// Core directories and paths
|
|
452
|
-
'projectRoot', 'sourceDir', 'i18nDir', 'outputDir', 'backupDir', 'tempDir', 'cacheDir', 'configDir',
|
|
453
|
-
// Language settings
|
|
454
|
-
'sourceLanguage', 'uiLanguage', 'language', 'defaultLanguages', 'supportedLanguages',
|
|
455
|
-
// Translation markers and content
|
|
456
|
-
'notTranslatedMarker', 'notTranslatedMarkers', 'translatedMarker', 'translatedMarkers',
|
|
457
|
-
// File handling
|
|
458
|
-
'supportedExtensions', 'excludeFiles', 'excludeDirs', 'includeFiles', 'includeDirs',
|
|
459
|
-
// Operational settings
|
|
460
|
-
'strictMode', 'debug', 'displayPaths', 'version', 'scriptDirectories',
|
|
461
|
-
// Framework and processing
|
|
462
|
-
'framework', 'processing', 'performance', 'advanced',
|
|
463
|
-
// UI and theme settings
|
|
464
|
-
'theme', 'ui', 'setup', 'reports', 'display', 'interface',
|
|
465
|
-
// Security and settings
|
|
466
|
-
'security', 'settings', 'preferences', 'config', 'configuration',
|
|
467
|
-
// Additional common properties
|
|
468
|
-
'autoSave', 'autoBackup', 'validateOnSave', 'showWarnings', 'verbose',
|
|
469
|
-
'timeout', 'retries', 'batchSize', 'maxConcurrency', 'cacheEnabled'
|
|
470
|
-
]);
|
|
471
|
-
|
|
472
|
-
// Remove unknown properties
|
|
473
|
-
Object.keys(sanitized).forEach(key => {
|
|
474
|
-
if (!allowedProperties.has(key)) {
|
|
475
|
-
// Only log warnings for properties that might be security risks
|
|
476
|
-
const value = sanitized[key];
|
|
477
|
-
const isSuspicious = typeof value === 'string' &&
|
|
478
|
-
(value.includes('..') || value.includes('/') || value.includes('\\') ||
|
|
479
|
-
value.includes('$') || value.includes('`') || value.includes('|') ||
|
|
480
|
-
value.includes(';') || value.includes('&'));
|
|
481
|
-
|
|
482
|
-
if (isSuspicious) {
|
|
483
|
-
SecurityUtils.logSecurityEvent('Removing potentially suspicious configuration property', 'warn', {
|
|
484
|
-
property: key,
|
|
485
|
-
value: sanitized[key]
|
|
486
|
-
});
|
|
487
|
-
} else {
|
|
488
|
-
// Use info level for normal unknown properties to reduce noise
|
|
489
|
-
SecurityUtils.logSecurityEvent('Removing unknown configuration property', 'info', {
|
|
490
|
-
property: key,
|
|
491
|
-
value: sanitized[key]
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
delete sanitized[key];
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
// Validate and sanitize path properties
|
|
499
|
-
const pathProperties = ['projectRoot', 'sourceDir', 'i18nDir', 'outputDir', 'backupDir', 'tempDir', 'cacheDir', 'configDir'];
|
|
500
|
-
|
|
501
|
-
pathProperties.forEach(prop => {
|
|
502
|
-
if (sanitized[prop] && typeof sanitized[prop] === 'string') {
|
|
503
|
-
// Check for dangerous patterns
|
|
504
|
-
if (!SecurityUtils.isSafePath(sanitized[prop])) {
|
|
505
|
-
SecurityUtils.logSecurityEvent('Path validation failed for configuration property', 'error', {
|
|
506
|
-
property: prop,
|
|
507
|
-
originalPath: sanitized[prop]
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// Attempt to sanitize the path by removing dangerous patterns
|
|
511
|
-
let sanitizedPath = sanitized[prop];
|
|
512
|
-
|
|
513
|
-
// Remove parent directory references (path traversal)
|
|
514
|
-
sanitizedPath = sanitizedPath.replace(/\.\.[\/\\]/g, '');
|
|
515
|
-
|
|
516
|
-
// Remove shell metacharacters and dangerous patterns
|
|
517
|
-
sanitizedPath = sanitizedPath.replace(/[|;&$`{}()[\]<>?]/g, '');
|
|
518
|
-
|
|
519
|
-
// Only remove absolute path indicators if they're suspicious
|
|
520
|
-
// Allow legitimate Windows drive letters (C:\, D:\, etc.) but remove suspicious ones
|
|
521
|
-
if (sanitizedPath.match(/^[A-Z]:[\/\\]/)) {
|
|
522
|
-
// Check if this looks like a legitimate Windows path by ensuring it doesn't contain
|
|
523
|
-
// suspicious patterns after the drive letter
|
|
524
|
-
const afterDrive = sanitizedPath.substring(3); // Everything after "C:\"
|
|
525
|
-
if (afterDrive.includes('..') || afterDrive.match(/[|;&$`{}()[\]<>?]/)) {
|
|
526
|
-
// Remove the drive letter if the rest of the path is suspicious
|
|
527
|
-
sanitizedPath = sanitizedPath.replace(/^[A-Z]:[\/\\]/, '');
|
|
528
|
-
}
|
|
529
|
-
// If the path looks legitimate, keep the drive letter
|
|
530
|
-
} else {
|
|
531
|
-
// For non-Windows paths, remove leading slashes as before
|
|
532
|
-
sanitizedPath = sanitizedPath.replace(/^[\/\\]/, '');
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (sanitizedPath !== sanitized[prop]) {
|
|
536
|
-
// Only warn if significant changes were made (not just removing drive letters for legitimate paths)
|
|
537
|
-
const significantChange = sanitizedPath.length < sanitized[prop].length * 0.8 ||
|
|
538
|
-
sanitizedPath.replace(/[\/\\]/g, '') !== sanitized[prop].replace(/^[A-Z]:[\/\\]/, '').replace(/[\/\\]/g, '');
|
|
539
|
-
|
|
540
|
-
if (significantChange) {
|
|
541
|
-
SecurityUtils.logSecurityEvent('Path sanitized for configuration property', 'warn', {
|
|
542
|
-
property: prop,
|
|
543
|
-
originalPath: sanitized[prop],
|
|
544
|
-
sanitizedPath: sanitizedPath
|
|
545
|
-
});
|
|
546
|
-
} else {
|
|
547
|
-
SecurityUtils.logSecurityEvent('Path normalized for configuration property', 'info', {
|
|
548
|
-
property: prop,
|
|
549
|
-
originalPath: sanitized[prop],
|
|
550
|
-
sanitizedPath: sanitizedPath
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
sanitized[prop] = sanitizedPath;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
// Validate security settings
|
|
560
|
-
if (sanitized.security) {
|
|
561
|
-
const security = sanitized.security;
|
|
562
|
-
|
|
563
|
-
// Validate session timeout (should be reasonable)
|
|
564
|
-
if (security.sessionTimeout && (typeof security.sessionTimeout !== 'number' || security.sessionTimeout < 60000 || security.sessionTimeout > 86400000)) {
|
|
565
|
-
SecurityUtils.logSecurityEvent('Invalid session timeout in security configuration', 'warn', {
|
|
566
|
-
sessionTimeout: security.sessionTimeout
|
|
567
|
-
});
|
|
568
|
-
security.sessionTimeout = 1800000; // Default to 30 minutes
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// Validate max failed attempts
|
|
572
|
-
if (security.maxFailedAttempts && (typeof security.maxFailedAttempts !== 'number' || security.maxFailedAttempts < 1 || security.maxFailedAttempts > 10)) {
|
|
573
|
-
SecurityUtils.logSecurityEvent('Invalid max failed attempts in security configuration', 'warn', {
|
|
574
|
-
maxFailedAttempts: security.maxFailedAttempts
|
|
575
|
-
});
|
|
576
|
-
security.maxFailedAttempts = 3; // Default to 3 attempts
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Validate language settings
|
|
581
|
-
if (sanitized.sourceLanguage && typeof sanitized.sourceLanguage === 'string') {
|
|
582
|
-
// Sanitize language code (only allow alphanumeric, hyphens, underscores)
|
|
583
|
-
sanitized.sourceLanguage = sanitized.sourceLanguage.replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
if (sanitized.uiLanguage && typeof sanitized.uiLanguage === 'string') {
|
|
587
|
-
sanitized.uiLanguage = sanitized.uiLanguage.replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Validate default languages array
|
|
591
|
-
if (sanitized.defaultLanguages && Array.isArray(sanitized.defaultLanguages)) {
|
|
592
|
-
sanitized.defaultLanguages = sanitized.defaultLanguages
|
|
593
|
-
.filter(lang => typeof lang === 'string')
|
|
594
|
-
.map(lang => lang.replace(/[^a-zA-Z0-9\-_]/g, ''))
|
|
595
|
-
.filter(lang => lang.length > 0);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
SecurityUtils.logSecurityEvent('Configuration validation completed', 'info', {
|
|
599
|
-
propertiesCount: Object.keys(sanitized).length,
|
|
600
|
-
sanitizedPaths: pathProperties.filter(prop => sanitized[prop]).length
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
return sanitized;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
module.exports = SecurityUtils;
|