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