UNPKG

i18ntk

Version:

i18ntk (i18n Toolkit) - A comprehensive, enterprise-grade internationalization (i18n) management toolkit for JavaScript/TypeScript projects with advanced analysis, validation, and automation features

374 lines (330 loc) 12.8 kB
const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const SettingsManager = require('../settings/settings-manager'); const i18n = require('./i18n-helper'); /** * 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') { SecurityUtils.logSecurityEvent(i18n.t('security.pathValidationFailed'), 'error', { inputPath: filePath, reason: i18n.t('security.invalidInputType') }); return null; } // Sanitize the input path const sanitizedPath = path.normalize(filePath).replace(/\.\./g, ''); // Resolve the full path const resolvedPath = path.resolve(basePath, sanitizedPath); // Check if the resolved path is within the base path const relativePath = path.relative(basePath, resolvedPath); if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { SecurityUtils.logSecurityEvent(i18n.t('security.pathTraversalAttempt'), 'warning', { inputPath: filePath, resolvedPath, basePath }); return null; } SecurityUtils.logSecurityEvent(i18n.t('security.pathValidated'), 'info', { inputPath: filePath, resolvedPath }); return resolvedPath; } catch (error) { SecurityUtils.logSecurityEvent(i18n.t('security.pathValidationError'), 'error', { inputPath: filePath, error: error.message }); return null; } } /** * 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) { console.warn(i18n.t('security.file_too_large', { filePath: validatedPath })); return null; } return await fs.promises.readFile(validatedPath, encoding); } catch (error) { 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; } 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) { 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) { 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) { console.warn(i18n.t('security.json_string_too_large')); return null; } try { return JSON.parse(jsonString); } catch (error) { 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 if (!allowedChars.test(sanitized)) { console.warn(i18n.t('security.inputDisallowedCharacters')); // Remove disallowed characters 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 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) { if (!config || typeof config !== 'object') { return null; } const validatedConfig = {}; const allowedKeys = [ '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', 'advanced', 'security', 'debug' ]; for (const [key, value] of Object.entries(config)) { if (!allowedKeys.includes(key)) { 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\-_\.\,\/\\\:]+$/, 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'); } /** * 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 settingsManager = SettingsManager; if (settingsManager.shouldShowSecurityLogs()) { 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')) { console.log(i18n.t('security.security_alert', { timestamp, event }), details); } } } } module.exports = SecurityUtils;