UNPKG

@ordojs/security

Version:

Security package for OrdoJS with XSS, CSRF, and injection protection

210 lines 7.93 kB
/** * Path Traversal Prevention * Provides comprehensive protection against path traversal attacks */ import * as path from 'path'; export class PathTraversalPrevention { static DANGEROUS_PATTERNS = [ // Directory traversal patterns /\.\./g, /\.\.\/|\.\.\\/, /\.\.\//g, /\.\.\\/g, // URL encoded traversal /%2e%2e%2f/gi, /%2e%2e%5c/gi, /%2e%2e/gi, // Double URL encoded /%252e%252e%252f/gi, /%252e%252e%255c/gi, // Unicode encoded /\u002e\u002e\u002f/g, /\u002e\u002e\u005c/g, // Null byte injection /\x00/g, /%00/gi, // Absolute path attempts /^\/|^[a-zA-Z]:\\/, // UNC path attempts (Windows) /^\\\\|^\\\\/, // Stream access (Windows) /:/g ]; static DANGEROUS_EXTENSIONS = [ '.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', '.jar', '.php', '.asp', '.aspx', '.jsp', '.py', '.rb', '.pl', '.sh', '.bash' ]; static SYSTEM_DIRECTORIES = [ 'windows', 'system32', 'program files', 'programdata', 'etc', 'bin', 'sbin', 'usr', 'var', 'tmp', 'boot', 'proc', 'sys', 'dev', 'root' ]; /** * Validates a file path for traversal attacks */ static validatePath(inputPath, options = {}) { const errors = []; if (!inputPath || typeof inputPath !== 'string') { errors.push('Path must be a non-empty string'); return { isValid: false, errors }; } // Check for dangerous patterns for (const pattern of this.DANGEROUS_PATTERNS) { if (pattern.test(inputPath)) { errors.push(`Dangerous path pattern detected: ${pattern.source}`); } } // Check for null bytes and control characters if (/[\x00-\x1f\x7f-\x9f]/.test(inputPath)) { errors.push('Path contains control characters or null bytes'); } // Check path length if (inputPath.length > 260) { // Windows MAX_PATH limit errors.push('Path exceeds maximum allowed length'); } // Normalize and resolve the path let normalizedPath; try { normalizedPath = path.normalize(inputPath); // Check if normalization revealed traversal attempts if (normalizedPath.includes('..')) { errors.push('Path contains directory traversal after normalization'); } } catch (error) { errors.push('Path normalization failed'); return { isValid: false, errors }; } // Check absolute vs relative path restrictions const isAbsolute = path.isAbsolute(normalizedPath); if (isAbsolute && !options.allowAbsolute) { errors.push('Absolute paths are not allowed'); } if (!isAbsolute && options.allowRelative === false) { errors.push('Relative paths are not allowed'); } // Check base path restriction if (options.basePath && isAbsolute) { const resolvedBase = path.resolve(options.basePath); const resolvedPath = path.resolve(normalizedPath); if (!resolvedPath.startsWith(resolvedBase)) { errors.push('Path is outside allowed base directory'); } } // Check file extension const ext = path.extname(normalizedPath).toLowerCase(); if (ext) { if (options.blockedExtensions?.includes(ext)) { errors.push(`File extension '${ext}' is not allowed`); } if (options.allowedExtensions && !options.allowedExtensions.includes(ext)) { errors.push(`File extension '${ext}' is not in allowed list`); } if (this.DANGEROUS_EXTENSIONS.includes(ext)) { errors.push(`Potentially dangerous file extension '${ext}' detected`); } } // Check directory depth if (options.maxDepth) { const depth = normalizedPath.split(path.sep).length - 1; if (depth > options.maxDepth) { errors.push(`Path depth (${depth}) exceeds maximum allowed (${options.maxDepth})`); } } // Check for system directories const pathParts = normalizedPath.toLowerCase().split(path.sep); for (const systemDir of this.SYSTEM_DIRECTORIES) { if (pathParts.includes(systemDir)) { errors.push(`Access to system directory '${systemDir}' is not allowed`); } } return { isValid: errors.length === 0, errors, ...(errors.length === 0 && { sanitizedPath: normalizedPath }) }; } /** * Sanitizes a path by removing dangerous elements */ static sanitizePath(inputPath, options = {}) { if (!inputPath || typeof inputPath !== 'string') { return ''; } let sanitized = inputPath; // Remove dangerous patterns for (const pattern of this.DANGEROUS_PATTERNS) { sanitized = sanitized.replace(pattern, ''); } // Remove control characters and null bytes sanitized = sanitized.replace(/[\x00-\x1f\x7f-\x9f]/g, ''); // Remove multiple consecutive slashes/backslashes sanitized = sanitized.replace(/[\/\\]+/g, path.sep); // Remove leading/trailing whitespace and path separators sanitized = sanitized.trim().replace(/^[\/\\]+|[\/\\]+$/g, ''); // Normalize the path try { sanitized = path.normalize(sanitized); } catch { // If normalization fails, return empty string return ''; } // Ensure it doesn't start with .. after sanitization if (sanitized.startsWith('..')) { sanitized = sanitized.replace(/^\.\.+[\/\\]?/, ''); } // Apply length limit if (sanitized.length > 255) { sanitized = sanitized.substring(0, 255); } return sanitized; } /** * Creates a safe filename from user input */ static createSafeFilename(filename, options = {}) { if (!filename || typeof filename !== 'string') { return 'untitled'; } const maxLength = options.maxLength || 100; let safe = filename; // Remove path separators and dangerous characters safe = safe.replace(/[\/\\:*?"<>|]/g, '_'); // Remove control characters safe = safe.replace(/[\x00-\x1f\x7f-\x9f]/g, ''); // Remove leading/trailing dots and spaces safe = safe.replace(/^[\.\s]+|[\.\s]+$/g, ''); // Ensure it's not empty after sanitization if (!safe) { safe = 'untitled'; } // Handle extension const ext = path.extname(safe).toLowerCase(); const basename = path.basename(safe, ext); let finalExt = ext; if (options.allowedExtensions && ext && !options.allowedExtensions.includes(ext)) { finalExt = options.defaultExtension || '.txt'; } // Truncate if too long const maxBasenameLength = maxLength - finalExt.length; const truncatedBasename = basename.length > maxBasenameLength ? basename.substring(0, maxBasenameLength) : basename; return truncatedBasename + finalExt; } /** * Validates multiple paths at once */ static validatePaths(paths, options = {}) { const results = paths.map(inputPath => ({ path: inputPath, ...this.validatePath(inputPath, options) })); return { isValid: results.every(result => result.isValid), results }; } } //# sourceMappingURL=path-traversal-prevention.js.map