i18ntk 2.3.7 → 2.4.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 +9 -6
- package/main/i18ntk-backup-class.js +35 -423
- package/main/manage/commands/BackupCommand.js +62 -62
- package/main/manage/services/SetupService.js +444 -462
- package/package.json +12 -9
- package/utils/config-manager.js +84 -30
- package/utils/config.js +15 -14
- package/utils/i18n-helper.js +35 -20
- package/utils/logger.js +233 -64
- package/utils/npm-version-warning.js +12 -141
- package/utils/security.js +233 -150
package/utils/security.js
CHANGED
|
@@ -1,26 +1,93 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { logger } = require('./logger');
|
|
5
|
+
|
|
6
|
+
const INTERNAL_MANIFEST_CACHE = {
|
|
7
|
+
initialized: false,
|
|
8
|
+
roots: new Set()
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function findPackageRoot(startDir) {
|
|
12
|
+
let current = path.resolve(startDir || process.cwd());
|
|
13
|
+
while (true) {
|
|
14
|
+
const manifest = path.join(current, 'package.json');
|
|
15
|
+
if (fs.existsSync(manifest)) {
|
|
16
|
+
return current;
|
|
17
|
+
}
|
|
18
|
+
const parent = path.dirname(current);
|
|
19
|
+
if (parent === current) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
current = parent;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function initializeInternalRoots() {
|
|
27
|
+
if (INTERNAL_MANIFEST_CACHE.initialized) {
|
|
28
|
+
return INTERNAL_MANIFEST_CACHE.roots;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
INTERNAL_MANIFEST_CACHE.initialized = true;
|
|
32
|
+
const roots = INTERNAL_MANIFEST_CACHE.roots;
|
|
33
|
+
const candidates = [
|
|
34
|
+
process.cwd(),
|
|
35
|
+
path.resolve(__dirname, '..'),
|
|
36
|
+
findPackageRoot(process.cwd()),
|
|
37
|
+
findPackageRoot(path.resolve(__dirname, '..'))
|
|
38
|
+
].filter(Boolean);
|
|
39
|
+
|
|
40
|
+
for (const candidate of candidates) {
|
|
41
|
+
roots.add(path.resolve(candidate));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const custom = String(process.env.I18NTK_INTERNAL_PATH_PREFIXES || '')
|
|
45
|
+
.split(',')
|
|
46
|
+
.map((entry) => entry.trim())
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
for (const prefix of custom) {
|
|
49
|
+
roots.add(path.resolve(prefix));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return roots;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeForCompare(value) {
|
|
56
|
+
return String(value || '').replace(/\\/g, '/').toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isPathInside(root, target) {
|
|
60
|
+
const normalizedRoot = normalizeForCompare(path.resolve(root));
|
|
61
|
+
const normalizedTarget = normalizeForCompare(path.resolve(target));
|
|
62
|
+
return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(`${normalizedRoot}/`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isInternalPath(inputPath) {
|
|
66
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const roots = initializeInternalRoots();
|
|
70
|
+
const absolute = path.resolve(inputPath);
|
|
71
|
+
for (const root of roots) {
|
|
72
|
+
if (isPathInside(root, absolute)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function detectDangerReason(filePath) {
|
|
80
|
+
if (/\.\.(\/|\\|$)/.test(filePath)) return 'Contains parent directory traversal segments';
|
|
81
|
+
if (/(^|[\/\\])~([\/\\]|$)/.test(filePath)) return 'Contains home-directory shorthand';
|
|
82
|
+
if (/\$\{/.test(filePath)) return 'Contains variable expansion token';
|
|
83
|
+
if (/`/.test(filePath)) return 'Contains command substitution token';
|
|
84
|
+
if (/[|;&<>]/.test(filePath)) return 'Contains shell metacharacters';
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
// Lazy load i18n to prevent initialization race conditions
|
|
90
|
+
let i18n;
|
|
24
91
|
function getI18n() {
|
|
25
92
|
if (!i18n) {
|
|
26
93
|
try {
|
|
@@ -74,11 +141,13 @@ static _logging = false;
|
|
|
74
141
|
SecurityUtils._operationStack = new Set();
|
|
75
142
|
}
|
|
76
143
|
|
|
77
|
-
if (SecurityUtils._operationStack.has(operationName)) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
144
|
+
if (SecurityUtils._operationStack.has(operationName)) {
|
|
145
|
+
SecurityUtils.logSecurityEvent('Recursion detected while performing secure operation', 'warn', {
|
|
146
|
+
operation: operationName,
|
|
147
|
+
source: 'internal'
|
|
148
|
+
});
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
82
151
|
|
|
83
152
|
SecurityUtils._operationStack.add(operationName);
|
|
84
153
|
|
|
@@ -88,12 +157,15 @@ static _logging = false;
|
|
|
88
157
|
let hasResult = false;
|
|
89
158
|
let timeoutId = null;
|
|
90
159
|
|
|
91
|
-
timeoutId = setTimeout(() => {
|
|
92
|
-
if (!hasResult) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
160
|
+
timeoutId = setTimeout(() => {
|
|
161
|
+
if (!hasResult) {
|
|
162
|
+
SecurityUtils.logSecurityEvent('Secure operation timeout', 'warn', {
|
|
163
|
+
operation: operationName,
|
|
164
|
+
timeoutMs,
|
|
165
|
+
source: 'internal'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}, timeoutMs);
|
|
97
169
|
|
|
98
170
|
// Execute operation synchronously
|
|
99
171
|
result = operation();
|
|
@@ -105,12 +177,12 @@ static _logging = false;
|
|
|
105
177
|
|
|
106
178
|
return result;
|
|
107
179
|
} catch (error) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
180
|
+
SecurityUtils.logSecurityEvent('Secure operation error', 'error', {
|
|
181
|
+
operation: operationName,
|
|
182
|
+
error: error.message,
|
|
183
|
+
source: 'internal'
|
|
184
|
+
});
|
|
185
|
+
return null;
|
|
114
186
|
} finally {
|
|
115
187
|
SecurityUtils._operationStack.delete(operationName);
|
|
116
188
|
}
|
|
@@ -122,60 +194,40 @@ static _logging = false;
|
|
|
122
194
|
* @param {string} level - Log level (info, warn, error)
|
|
123
195
|
* @param {object} details - Additional details
|
|
124
196
|
*/
|
|
125
|
-
static logSecurityEvent(event, level = 'info', details = {}) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
event,
|
|
153
|
-
details: {
|
|
154
|
-
...details,
|
|
155
|
-
pid: process.pid,
|
|
156
|
-
nodeVersion: process.version
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const message = `[SECURITY ${level.toUpperCase()}] ${timestamp}: ${event}`;
|
|
161
|
-
if (level === 'error') {
|
|
162
|
-
console.error(message, details);
|
|
163
|
-
} else if (level === 'warn' || level === 'warning') {
|
|
164
|
-
console.warn(message, details);
|
|
165
|
-
} else {
|
|
166
|
-
console.log(message, details);
|
|
167
|
-
}
|
|
168
|
-
} finally {
|
|
169
|
-
SecurityUtils._logging = false;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
197
|
+
static logSecurityEvent(event, level = 'info', details = {}) {
|
|
198
|
+
if (SecurityUtils._logging) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
SecurityUtils._logging = true;
|
|
203
|
+
try {
|
|
204
|
+
const debugMode = logger.isDebugMode();
|
|
205
|
+
const explicitSecurityLogs = process.env.I18NTK_ENABLE_SECURITY_LOGS === 'true';
|
|
206
|
+
const source = details && details.source ? String(details.source).toLowerCase() : 'internal';
|
|
207
|
+
const levelName = String(level || 'info').toLowerCase();
|
|
208
|
+
const normalizedLevel = levelName === 'warning' ? 'warn' : levelName;
|
|
209
|
+
|
|
210
|
+
// Security warnings from internal paths are noise during builds.
|
|
211
|
+
if (!debugMode && !explicitSecurityLogs && source !== 'user') {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
logger.security(normalizedLevel, event, {
|
|
216
|
+
...details,
|
|
217
|
+
pid: process.pid,
|
|
218
|
+
nodeVersion: process.version
|
|
219
|
+
});
|
|
220
|
+
} finally {
|
|
221
|
+
SecurityUtils._logging = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
172
224
|
|
|
173
225
|
// Add other static methods here...
|
|
174
|
-
static validatePath(filePath, basePath = process.cwd(), verbose = false) {
|
|
175
|
-
const i18n = getI18n();
|
|
176
|
-
const useI18n = i18n && i18n.isInitialized && typeof i18n.t === 'function';
|
|
177
|
-
|
|
178
|
-
try {
|
|
226
|
+
static validatePath(filePath, basePath = process.cwd(), verbose = false) {
|
|
227
|
+
const i18n = getI18n();
|
|
228
|
+
const useI18n = i18n && i18n.isInitialized && typeof i18n.t === 'function';
|
|
229
|
+
|
|
230
|
+
try {
|
|
179
231
|
// Check against whitelist patterns for our own package artifacts
|
|
180
232
|
if (SecurityUtils.PACKAGE_ARTIFACT_WHITELIST.some(pattern => pattern.test(filePath))) {
|
|
181
233
|
return filePath;
|
|
@@ -195,17 +247,42 @@ static _logging = false;
|
|
|
195
247
|
return null;
|
|
196
248
|
}
|
|
197
249
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
250
|
+
const isWindowsAbsolute = /^[A-Z]:[\/\\]/i.test(filePath);
|
|
251
|
+
const isUnixAbsolute = filePath.startsWith('/') || filePath.startsWith('\\');
|
|
252
|
+
const isAbsolutePath = isWindowsAbsolute || isUnixAbsolute;
|
|
253
|
+
const dangerousReason = detectDangerReason(filePath);
|
|
254
|
+
const source = isInternalPath(filePath) ? 'internal' : 'user';
|
|
255
|
+
|
|
256
|
+
if (isAbsolutePath && isInternalPath(filePath)) {
|
|
257
|
+
return path.resolve(filePath);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// For absolute paths, defer trust decision to base-path containment checks below,
|
|
261
|
+
// but still reject obvious shell/injection markers.
|
|
262
|
+
if (isAbsolutePath && dangerousReason) {
|
|
263
|
+
const message = useI18n
|
|
264
|
+
? i18n.t('security.pathTraversalAttempt')
|
|
265
|
+
: 'Path traversal attempt';
|
|
266
|
+
SecurityUtils.logSecurityEvent(message, 'warning', {
|
|
267
|
+
inputPath: filePath,
|
|
268
|
+
reason: dangerousReason,
|
|
269
|
+
source
|
|
270
|
+
});
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check for obvious dangerous patterns first for relative paths
|
|
275
|
+
if (!isAbsolutePath && !SecurityUtils.isSafePath(filePath)) {
|
|
276
|
+
const message = useI18n
|
|
277
|
+
? i18n.t('security.pathTraversalAttempt')
|
|
278
|
+
: 'Path traversal attempt';
|
|
279
|
+
SecurityUtils.logSecurityEvent(message, 'warning', {
|
|
280
|
+
inputPath: filePath,
|
|
281
|
+
reason: dangerousReason || 'Contains unsafe path segments',
|
|
282
|
+
source
|
|
283
|
+
});
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
209
286
|
|
|
210
287
|
// Resolve base and target paths
|
|
211
288
|
const base = fs.realpathSync(basePath);
|
|
@@ -220,44 +297,47 @@ static _logging = false;
|
|
|
220
297
|
}
|
|
221
298
|
|
|
222
299
|
// Check for actual path traversal (going outside the base directory)
|
|
223
|
-
const relativePath = path.relative(base, finalPath);
|
|
224
|
-
if (relativePath.startsWith('..')) {
|
|
225
|
-
const message = useI18n
|
|
226
|
-
? i18n.t('security.pathTraversalAttempt')
|
|
227
|
-
: 'Path traversal attempt';
|
|
228
|
-
SecurityUtils.logSecurityEvent(message, 'warning', {
|
|
229
|
-
inputPath: filePath,
|
|
230
|
-
resolvedPath: finalPath,
|
|
231
|
-
basePath: base,
|
|
232
|
-
relativePath: relativePath
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
300
|
+
const relativePath = path.relative(base, finalPath);
|
|
301
|
+
if (relativePath.startsWith('..')) {
|
|
302
|
+
const message = useI18n
|
|
303
|
+
? i18n.t('security.pathTraversalAttempt')
|
|
304
|
+
: 'Path traversal attempt';
|
|
305
|
+
SecurityUtils.logSecurityEvent(message, 'warning', {
|
|
306
|
+
inputPath: filePath,
|
|
307
|
+
resolvedPath: finalPath,
|
|
308
|
+
basePath: base,
|
|
309
|
+
relativePath: relativePath,
|
|
310
|
+
source
|
|
311
|
+
});
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
236
314
|
|
|
237
315
|
// Allow absolute paths that resolve within the project structure
|
|
238
316
|
// The isSafePath check above already filtered out dangerous absolute paths
|
|
239
317
|
|
|
240
318
|
if (verbose) {
|
|
241
|
-
const successMsg = useI18n
|
|
242
|
-
? i18n.t('security.pathValidated')
|
|
243
|
-
: 'Path validated';
|
|
244
|
-
SecurityUtils.logSecurityEvent(successMsg, 'info', {
|
|
245
|
-
inputPath: filePath,
|
|
246
|
-
resolvedPath: finalPath
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
319
|
+
const successMsg = useI18n
|
|
320
|
+
? i18n.t('security.pathValidated')
|
|
321
|
+
: 'Path validated';
|
|
322
|
+
SecurityUtils.logSecurityEvent(successMsg, 'info', {
|
|
323
|
+
inputPath: filePath,
|
|
324
|
+
resolvedPath: finalPath,
|
|
325
|
+
source
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return finalPath;
|
|
250
329
|
} catch (error) {
|
|
251
330
|
const message = useI18n
|
|
252
331
|
? i18n.t('security.pathValidationError')
|
|
253
332
|
: 'Path validation error';
|
|
254
|
-
SecurityUtils.logSecurityEvent(message, 'error', {
|
|
255
|
-
inputPath: filePath,
|
|
256
|
-
error: error.message
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
333
|
+
SecurityUtils.logSecurityEvent(message, 'error', {
|
|
334
|
+
inputPath: filePath,
|
|
335
|
+
error: error.message,
|
|
336
|
+
source: isInternalPath(filePath) ? 'internal' : 'user'
|
|
337
|
+
});
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
261
341
|
|
|
262
342
|
static safeExistsSync(filePath, basePath, timeoutMs = 3000) {
|
|
263
343
|
return this.withTimeoutSync(() => {
|
|
@@ -489,21 +569,23 @@ static _logging = false;
|
|
|
489
569
|
const resolvedPath = path.resolve(joinedPath);
|
|
490
570
|
|
|
491
571
|
// Ensure the final path is within the base directory
|
|
492
|
-
if (!resolvedPath.startsWith(resolvedBase)) {
|
|
493
|
-
SecurityUtils.logSecurityEvent('Path traversal attempt detected in safeJoin', 'error', {
|
|
494
|
-
basePath,
|
|
495
|
-
paths,
|
|
496
|
-
resolvedPath
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
572
|
+
if (!resolvedPath.startsWith(resolvedBase)) {
|
|
573
|
+
SecurityUtils.logSecurityEvent('Path traversal attempt detected in safeJoin', 'error', {
|
|
574
|
+
basePath,
|
|
575
|
+
paths,
|
|
576
|
+
resolvedPath,
|
|
577
|
+
source: 'internal'
|
|
578
|
+
});
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
500
581
|
return resolvedPath;
|
|
501
582
|
} catch (error) {
|
|
502
|
-
SecurityUtils.logSecurityEvent('Error in safeJoin', 'error', {
|
|
503
|
-
basePath,
|
|
504
|
-
paths,
|
|
505
|
-
error: error.message
|
|
506
|
-
|
|
583
|
+
SecurityUtils.logSecurityEvent('Error in safeJoin', 'error', {
|
|
584
|
+
basePath,
|
|
585
|
+
paths,
|
|
586
|
+
error: error.message,
|
|
587
|
+
source: 'internal'
|
|
588
|
+
});
|
|
507
589
|
return false;
|
|
508
590
|
}
|
|
509
591
|
}
|
|
@@ -551,11 +633,12 @@ static _logging = false;
|
|
|
551
633
|
];
|
|
552
634
|
|
|
553
635
|
// Allow absolute paths that are within the project structure
|
|
554
|
-
if (filePath.startsWith('/') || filePath.startsWith('\\')) {
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
636
|
+
if (filePath.startsWith('/') || filePath.startsWith('\\')) {
|
|
637
|
+
// Treat raw Unix-style absolute input as dangerous by default in this helper.
|
|
638
|
+
// `validatePath` can still permit absolute paths if they resolve within basePath.
|
|
639
|
+
const hasDangerousPatterns = dangerousPatterns.slice(1).some(pattern => pattern.test(filePath));
|
|
640
|
+
return !hasDangerousPatterns;
|
|
641
|
+
}
|
|
559
642
|
|
|
560
643
|
return !dangerousPatterns.some(pattern => pattern.test(filePath));
|
|
561
644
|
}
|