UNPKG

i18ntk

Version:

🚀 The fastest i18n toolkit with 97% performance boost! Zero-dependency, enterprise-grade internationalization for React, Vue, Angular, Python, Java, PHP & more. Features PIN protection, auto framework detection, 7+ UI languages, and comprehensive transla

603 lines (542 loc) • 19.9 kB
const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const configManager = require('./config-manager'); // Lazy load i18n to prevent initialization race conditions let i18n; function getI18n() { if (!i18n) { try { i18n = require('./i18n-helper'); } catch (error) { // Fallback to simple identity function if i18n fails to load console.warn('i18n-helper not available, using fallback messages'); return { t: (key, params = {}) => key }; } } return i18n; } /** * Security utility module for i18nTK * Provides secure file operations, path validation, and input sanitization * to prevent path traversal, code injection, and other security vulnerabilities */ class SecurityUtils { /** * Validates and sanitizes file paths to prevent path traversal attacks * @param {string} inputPath - The input path to validate * @param {string} basePath - The base path that the input should be within (optional) * @returns {string|null} - Sanitized path or null if invalid */ static validatePath(filePath, basePath = process.cwd()) { try { if (!filePath || typeof filePath !== 'string') { const i18n = getI18n(); SecurityUtils.logSecurityEvent(i18n.t('security.pathValidationFailed'), 'error', { inputPath: filePath, reason: i18n.t('security.invalidInputType') }); return null; } // Resolve base and target paths const base = fs.realpathSync(basePath); const resolvedPath = path.resolve(base, filePath); // Resolve symlinks if the path exists let finalPath = resolvedPath; try { finalPath = fs.realpathSync(resolvedPath); } catch { // If the path doesn't exist yet, fall back to the resolved path } // Ensure the target path is within the base directory const relativePath = path.relative(base, finalPath); if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { const i18n = getI18n(); SecurityUtils.logSecurityEvent(i18n.t('security.pathTraversalAttempt'), 'warning', { inputPath: filePath, resolvedPath: finalPath, basePath: base }); return null; } const i18n = getI18n(); SecurityUtils.logSecurityEvent(i18n.t('security.pathValidated'), 'info', { inputPath: filePath, resolvedPath: finalPath }); return finalPath; } catch (error) { const i18n = getI18n(); SecurityUtils.logSecurityEvent(i18n.t('security.pathValidationError'), 'error', { inputPath: filePath, error: error.message }); return null; } } /** * Safely checks if a path exists. * @param {string} filePath - Path to check. * @param {string} basePath - Base path for validation. * @returns {boolean} - True if the path exists and is safe. */ static safeExistsSync(filePath, basePath) { const validatedPath = this.validatePath(filePath, basePath); if (!validatedPath) { return false; } return fs.existsSync(validatedPath); } /** * Safely gets file stats. * @param {string} filePath - Path to get stats for. * @param {string} basePath - Base path for validation. * @returns {fs.Stats|null} - File stats or null if error. */ static safeStatSync(filePath, basePath) { const validatedPath = this.validatePath(filePath, basePath); if (!validatedPath) { return null; } try { return fs.statSync(validatedPath); } catch (error) { return null; } } /** * Safely creates a directory. * @param {string} dirPath - Path of the directory to create. * @param {string} basePath - Base path for validation. * @param {object} options - fs.mkdirSync options. * @returns {boolean} - True if successful. */ static safeMkdirSync(dirPath, basePath, options) { const validatedPath = this.validatePath(dirPath, basePath); if (!validatedPath) { return false; } try { fs.mkdirSync(validatedPath, options); return true; } catch (error) { if (error.code === 'EEXIST') { return true; // Already exists } return false; } } /** * Safely reads a file with path validation and error handling * @param {string} filePath - Path to the file * @param {string} basePath - Base path for validation * @param {string} encoding - File encoding (default: 'utf8') * @returns {Promise<string|null>} - File content or null if error */ static async safeReadFile(filePath, basePath, encoding = 'utf8') { const validatedPath = this.validatePath(filePath, basePath); if (!validatedPath) { return null; } try { // Check if file exists and is readable await fs.promises.access(validatedPath, fs.constants.R_OK); // Read file with size limit (10MB max) const stats = await fs.promises.stat(validatedPath); if (stats.size > 10 * 1024 * 1024) { const i18n = getI18n(); console.warn(i18n.t('security.file_too_large', { filePath: validatedPath })); return null; } return await fs.promises.readFile(validatedPath, encoding); } catch (error) { const i18n = getI18n(); console.warn(i18n.t('security.file_read_error', { errorMessage: error.message })); return null; } } /** * Safely reads a file synchronously with path validation and error handling * @param {string} filePath - Path to the file * @param {string} basePath - Base path for validation * @param {string} encoding - File encoding (default: 'utf8') * @returns {string|null} - File content or null if error */ static safeReadFileSync(filePath, basePath, encoding = 'utf8') { const validatedPath = this.validatePath(filePath, basePath); if (!validatedPath) { return null; } const i18n = getI18n(); try { // Check if file exists and is readable fs.accessSync(validatedPath, fs.constants.R_OK); // Read file with size limit (10MB max) const stats = fs.statSync(validatedPath); if (stats.size > 10 * 1024 * 1024) { console.warn(i18n.t('security.file_too_large', { filePath: validatedPath })); return null; } return fs.readFileSync(validatedPath, encoding); } catch (error) { console.warn(i18n.t('security.file_read_error', { errorMessage: error.message })); return null; } } /** * Safely writes a file with path validation and error handling * @param {string} filePath - Path to the file * @param {string} content - Content to write * @param {string} basePath - Base path for validation * @param {string} encoding - File encoding (default: 'utf8') * @returns {Promise<boolean>} - Success status */ static async safeWriteFile(filePath, content, basePath, encoding = 'utf8') { const validatedPath = this.validatePath(filePath, basePath); if (!validatedPath) { return false; } try { // Validate content size (10MB max) if (typeof content === 'string' && content.length > 10 * 1024 * 1024) { const i18n = getI18n(); console.warn(i18n.t('security.content_too_large_for_file', { filePath: validatedPath })); return false; } // Ensure directory exists const dir = path.dirname(validatedPath); await fs.promises.mkdir(dir, { recursive: true }); // Write file with proper permissions await fs.promises.writeFile(validatedPath, content, { encoding, mode: 0o644 }); return true; } catch (error) { const i18n = getI18n(); console.warn(i18n.t('security.file_write_error', { errorMessage: error.message })); return false; } } /** * Safely parses JSON with error handling and validation * @param {string} jsonString - JSON string to parse * @param {number} maxSize - Maximum allowed size (default: 1MB) * @returns {object|null} - Parsed object or null if error */ static safeParseJSON(jsonString, maxSize = 1024 * 1024) { if (!jsonString || typeof jsonString !== 'string') { return null; } if (jsonString.length > maxSize) { const i18n = getI18n(); console.warn(i18n.t('security.json_string_too_large')); return null; } try { return JSON.parse(jsonString); } catch (error) { const i18n = getI18n(); console.warn(i18n.t('security.json_parse_error', { errorMessage: error.message })); return null; } } /** * Sanitizes user input to prevent injection attacks * @param {string} input - User input to sanitize * @param {object} options - Sanitization options * @returns {string} - Sanitized input */ static sanitizeInput(input, options = {}) { if (!input || typeof input !== 'string') { return ''; } const { allowedChars = /^[a-zA-Z0-9\s\-_\.\,\!\?\(\)\[\]\{\}\:\;"'\/\\]+$/, maxLength = 1000, removeHTML = true, removeScripts = true } = options; let sanitized = input.trim(); // Limit length if (sanitized.length > maxLength) { sanitized = sanitized.substring(0, maxLength); } // Remove HTML tags if requested if (removeHTML) { sanitized = sanitized.replace(/<[^>]*>/g, ''); } // Remove script-like content if (removeScripts) { sanitized = sanitized.replace(/javascript:/gi, ''); sanitized = sanitized.replace(/on\w+\s*=/gi, ''); sanitized = sanitized.replace(/eval\s*\(/gi, ''); sanitized = sanitized.replace(/function\s*\(/gi, ''); } // Check against allowed characters - suppress warnings for normal operations if (!allowedChars.test(sanitized)) { // Skip warning for common file path characters and reduce verbosity const isFilePath = sanitized.includes('/') || sanitized.includes('\\') || sanitized.includes('.'); const isCommonContent = sanitized.length < 1000 && !sanitized.includes('<script'); if (!isFilePath && !isCommonContent) { const i18n = getI18n(); console.warn(i18n.t('security.inputDisallowedCharacters')); } // Allow more characters for file paths and content sanitized = sanitized.replace(/[^a-zA-Z0-9\s\-_\.\,\!\?\(\)\[\]\{\}\:\;"'\/\\]/g, ''); } return sanitized; } /** * Validates command line arguments * @param {object} args - Command line arguments * @returns {object} - Validated arguments */ static async validateCommandArgs(args) { const i18n = getI18n(); const validatedArgs = {}; const allowedArgs = [ 'source-dir', 'i18n-dir', 'output-dir', 'output-report', 'help', 'language', 'strict-mode', 'exclude-files', 'no-prompt' ]; for (const [key, value] of Object.entries(args)) { if (allowedArgs.includes(key)) { validatedArgs[key] = value; } else { console.warn(i18n.t('security.unknown_command_argument', { key })); } } return validatedArgs; } /** * Validates configuration object * @param {object} config - Configuration object * @returns {object|null} - Validated configuration or null if invalid */ static validateConfig(config) { const i18n = getI18n(); if (!config || typeof config !== 'object') { return null; } const validatedConfig = {}; const allowedKeys = [ 'version', 'sourceDir', 'outputDir', 'defaultLanguage', 'supportedLanguages', 'filePattern', 'excludePatterns', 'reportFormat', 'logLevel', 'i18nDir', 'sourceLanguage', 'excludeDirs', 'includeExtensions', 'translationPatterns', 'notTranslatedMarker', 'excludeFiles', 'strictMode', 'uiLanguage', 'language', 'sizeLimit', 'defaultLanguages', 'reportLanguage', 'theme', 'autoSave', 'notifications', 'dateFormat', 'timeFormat', 'timezone', 'processing', 'performance', 'advanced', 'security', 'debug', 'projectRoot', 'scriptDirectories', 'supportedExtensions', 'settings', 'backupDir', 'tempDir', 'cacheDir', 'configDir', 'displayPaths', 'reports', 'ui', 'behavior', 'dateTime', 'backup', 'framework', 'notTranslatedMarkers', 'placeholderStyles' ]; const strict = config.security?.strictConfig || false; for (const [key, value] of Object.entries(config)) { if (!allowedKeys.includes(key)) { if (strict) { console.warn(i18n.t('security.unknown_config_key', { key })); } continue; } // Validate specific config values switch (key) { case 'sourceDir': case 'outputDir': if (typeof value === 'string') { // Basic path validation - will be further validated when used validatedConfig[key] = this.sanitizeInput(value, { allowedChars: /^[a-zA-Z0-9\-_\.\,\/\\:\s]+$/, maxLength: 500 }); } break; case 'supportedLanguages': if (Array.isArray(value)) { validatedConfig[key] = value.filter(lang => typeof lang === 'string' && /^[a-z]{2}(-[A-Z]{2})?$/.test(lang) ); } break; case 'defaultLanguage': if (typeof value === 'string' && /^[a-z]{2}(-[A-Z]{2})?$/.test(value)) { validatedConfig[key] = value; } break; default: if (typeof value === 'string') { validatedConfig[key] = this.sanitizeInput(value); } else if (typeof value === 'boolean' || typeof value === 'number') { validatedConfig[key] = value; } } } return validatedConfig; } /** * Generates a secure hash for file integrity checking * @param {string} content - Content to hash * @returns {string} - SHA-256 hash */ static generateHash(content) { return crypto.createHash('sha256').update(content).digest('hex'); } /** * Securely saves an encrypted PIN to the settings directory * @param {string} pin - 4 digit PIN * @returns {Promise<boolean>} - success status */ static async saveEncryptedPin(pin) { try { const hash = crypto.createHash('sha256').update(pin).digest('hex'); const settingsDir = require('../settings/settings-manager').configDir; const pinFile = path.join(settingsDir, 'admin-pin.hash'); await fs.promises.mkdir(settingsDir, { recursive: true }); await fs.promises.writeFile(pinFile, hash, 'utf8'); return true; } catch (error) { return false; } } /** * Checks if a file path is safe for operations * @param {string} filePath - File path to check * @returns {boolean} - Whether the path is safe */ static isSafePath(filePath) { if (!filePath || typeof filePath !== 'string') { return false; } // Check for dangerous patterns const dangerousPatterns = [ /\.\./, // Parent directory traversal /^\//, // Absolute path (Unix) /^[A-Z]:\\/, // Absolute path (Windows) /~/, // Home directory /\$\{/, // Variable expansion /`/, // Command substitution /\|/, // Pipe /;/, // Command separator /&/, // Background process />/, // Redirect /</ // Redirect ]; return !dangerousPatterns.some(pattern => pattern.test(filePath)); } /** * Logs security events for monitoring * @param {string} event - Security event description * @param {string} level - Log level (info, warn, error) * @param {object} details - Additional details */ static logSecurityEvent(event, level = 'info', details = {}) { const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, event, details: { ...details, pid: process.pid, nodeVersion: process.version } }; // Only show security logs if debug mode is enabled and showSecurityLogs is true try { const cfg = configManager.getConfig(); if (cfg.debug?.enabled && cfg.debug?.showSecurityLogs) { console.log(`[SECURITY ${level.toUpperCase()}] ${timestamp}: ${event}`, details); } } catch (error) { // Fallback: if settings can't be loaded, don't show security logs to maintain clean UI // Only log critical security events in this case if (event.includes('CRITICAL') || event.includes('BREACH') || event.includes('ATTACK')) { const i18n = getI18n(); console.log(i18n.t('security.security_alert', { timestamp, event }), details); } } } /** * Safely reads directory contents with path validation * @param {string} dirPath - Directory path to read * @param {string} basePath - Base path for validation * @param {object} options - Options for readdirSync (withFileTypes, etc.) * @returns {Array|null} - Directory contents or null if error */ static safeReaddirSync(dirPath, basePath, options = {}) { const validatedPath = this.validatePath(dirPath, basePath); if (!validatedPath) { return null; } try { return fs.readdirSync(validatedPath, options); } catch (error) { const i18n = getI18n(); console.warn(i18n.t('security.directory_read_error', { errorMessage: error.message })); return null; } } /** * Secure performance measurement utility * Provides safe timing functionality without requiring perf_hooks * @returns {object} - Performance timing object */ static getPerformanceTimer() { // Use Date.now() as a fallback if perf_hooks is not available const hasPerfHooks = (() => { try { require('perf_hooks'); return true; } catch { return false; } })(); if (hasPerfHooks) { const { performance } = require('perf_hooks'); return { now: () => performance.now(), isHighResolution: true }; } else { return { now: () => Date.now(), isHighResolution: false }; } } /** * Secure debug logging utility * Provides controlled debug output based on configuration * @param {string} level - Log level (debug, info, warn, error) * @param {string} message - Log message * @param {object} details - Additional details */ static debugLog(level, message, details = {}) { try { const cfg = configManager.getConfig(); const debugEnabled = cfg.debug?.enabled || false; const logLevel = cfg.debug?.logLevel || 'info'; if (!debugEnabled) { return; } const levels = { debug: 0, info: 1, warn: 2, error: 3 }; const currentLevel = levels[logLevel] || 1; const messageLevel = levels[level] || 1; if (messageLevel < currentLevel) { return; } const timestamp = new Date().toISOString(); const logEntry = { timestamp, level: level.toUpperCase(), message, details, pid: process.pid }; // Only log to console if debug mode is enabled if (cfg.debug?.showConsoleOutput !== false) { console.log(`[${logEntry.level}] ${timestamp}: ${message}`, details); } // Log to file if configured if (cfg.debug?.logFile) { const fs = require('fs'); const path = require('path'); const logFile = path.resolve(cfg.debug.logFile); try { const logLine = JSON.stringify(logEntry) + '\n'; fs.appendFileSync(logFile, logLine); } catch (error) { // Silently fail if file logging fails } } } catch (error) { // Fallback: if config can't be loaded, don't crash console.warn(`[DEBUG] ${message}`, details); } } } module.exports = SecurityUtils;