UNPKG

quality-mcp

Version:

An MCP server that analyzes to your codebase, with plugin support for DCD and Simian. 🏍️ "The only Zen you find on the tops of mountains is the Zen you bring up there."

389 lines (329 loc) 11.4 kB
/** * Security utilities for input validation and path sanitization * Protects against path traversal attacks and validates user inputs */ import { resolve, isAbsolute, relative } from 'path'; import { existsSync, statSync } from 'fs'; import { createLogger } from './logger.js'; const logger = createLogger('security'); /** * Security validation errors */ export class SecurityValidationError extends Error { constructor(message, type = 'VALIDATION_ERROR') { super(message); this.name = 'SecurityValidationError'; this.type = type; } } /** * Configuration for path validation */ const DEFAULT_SECURITY_CONFIG = { // Maximum path length to prevent DoS attacks maxPathLength: 1000, // Allowed file extensions (empty array means all extensions allowed) allowedExtensions: [], // Blocked file extensions for security blockedExtensions: ['.exe', '.bat', '.cmd', '.sh', '.ps1', '.scr', '.vbs'], // Maximum file size for analysis (in bytes) - 100MB default maxFileSize: 100 * 1024 * 1024, // Allow absolute paths (should be false in production) allowAbsolutePaths: true, // Base directories where analysis is allowed (empty array means any directory) allowedBasePaths: [], // Blocked path patterns (regex strings) blockedPatterns: [ '/etc/', '/proc/', '/sys/', '/dev/', '/root/', '\\\\Windows\\\\', '\\\\Program Files\\\\', '\\\\System32\\\\', ], }; /** * Validate and sanitize a file or directory path * @param {string} inputPath - The path to validate * @param {Object} options - Validation options * @returns {string} Sanitized path * @throws {SecurityValidationError} If path is invalid or dangerous */ export function validatePath(inputPath, options = {}) { const config = { ...DEFAULT_SECURITY_CONFIG, ...options }; // Sanitize malformed config values if (config.maxPathLength <= 0) { config.maxPathLength = DEFAULT_SECURITY_CONFIG.maxPathLength; } if (!Array.isArray(config.blockedPatterns)) { config.blockedPatterns = DEFAULT_SECURITY_CONFIG.blockedPatterns; } // Basic input validation if (typeof inputPath !== 'string') { throw new SecurityValidationError('Path must be a string', 'INVALID_TYPE'); } if (inputPath.length === 0) { throw new SecurityValidationError('Path cannot be empty', 'EMPTY_PATH'); } if (inputPath.length > config.maxPathLength) { throw new SecurityValidationError( `Path too long (max ${config.maxPathLength} characters)`, 'PATH_TOO_LONG' ); } // Check for null bytes (potential injection) if (inputPath.includes('\0')) { throw new SecurityValidationError('Path contains null bytes', 'NULL_BYTE_INJECTION'); } // Normalize and resolve the path let normalizedPath; try { // For relative paths, resolve relative to current working directory if (isAbsolute(inputPath)) { normalizedPath = resolve(inputPath); } else { normalizedPath = resolve(process.cwd(), inputPath); } } catch (error) { throw new SecurityValidationError(`Invalid path format: ${error.message}`, 'INVALID_FORMAT'); } // Check for path traversal attempts const relativePath = relative(process.cwd(), normalizedPath); if (relativePath.startsWith('..') && !config.allowAbsolutePaths) { throw new SecurityValidationError('Path traversal attempt detected', 'PATH_TRAVERSAL'); } // Validate absolute paths if (isAbsolute(inputPath) && !config.allowAbsolutePaths) { throw new SecurityValidationError('Absolute paths not allowed', 'ABSOLUTE_PATH_BLOCKED'); } // Check against blocked patterns for (const pattern of config.blockedPatterns) { const regex = new RegExp(pattern, 'i'); if (regex.test(normalizedPath)) { throw new SecurityValidationError( `Path matches blocked pattern: ${pattern}`, 'BLOCKED_PATH_PATTERN' ); } } // Check allowed base paths if (config.allowedBasePaths.length > 0) { const isAllowed = config.allowedBasePaths.some(basePath => { const resolvedBase = resolve(basePath); return normalizedPath.startsWith(resolvedBase); }); if (!isAllowed) { throw new SecurityValidationError('Path outside allowed directories', 'PATH_OUTSIDE_ALLOWED'); } } // Check if path exists if (!existsSync(normalizedPath)) { throw new SecurityValidationError(`Path does not exist: ${normalizedPath}`, 'PATH_NOT_FOUND'); } // Validate file size for files try { const stats = statSync(normalizedPath); if (stats.isFile() && stats.size > config.maxFileSize) { throw new SecurityValidationError( `File too large (max ${config.maxFileSize} bytes)`, 'FILE_TOO_LARGE' ); } } catch (error) { throw new SecurityValidationError(`Cannot access path: ${error.message}`, 'ACCESS_ERROR'); } logger.debug(`Path validated: ${inputPath} -> ${normalizedPath}`); return normalizedPath; } /** * Validate file extensions * @param {string} filePath - Path to validate * @param {Array<string>} allowedExtensions - Allowed extensions (empty array allows all) * @param {Array<string>} blockedExtensions - Blocked extensions * @throws {SecurityValidationError} If extension is not allowed */ export function validateFileExtension(filePath, allowedExtensions = [], blockedExtensions = []) { const ext = filePath.toLowerCase().split('.').pop(); if (!ext) { return; // No extension is ok } // Check blocked extensions first if (blockedExtensions.includes(`.${ext}`)) { throw new SecurityValidationError( `File extension .${ext} is blocked for security`, 'BLOCKED_EXTENSION' ); } // Check allowed extensions if specified if (allowedExtensions.length > 0 && !allowedExtensions.includes(`.${ext}`)) { throw new SecurityValidationError( `File extension .${ext} is not allowed`, 'EXTENSION_NOT_ALLOWED' ); } } /** * Validate array of file extensions * @param {Array<string>} extensions - Extensions to validate * @returns {Array<string>} Sanitized extensions * @throws {SecurityValidationError} If any extension is invalid */ export function validateExtensions(extensions) { if (!Array.isArray(extensions)) { throw new SecurityValidationError('Extensions must be an array', 'INVALID_TYPE'); } const sanitized = []; for (const ext of extensions) { if (typeof ext !== 'string') { throw new SecurityValidationError('Extension must be a string', 'INVALID_TYPE'); } // Ensure extension starts with dot const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`; // Basic validation if (!/^\.[a-zA-Z0-9]+$/.test(normalizedExt)) { throw new SecurityValidationError( `Invalid extension format: ${ext}`, 'INVALID_EXTENSION_FORMAT' ); } // Check against blocked extensions if (DEFAULT_SECURITY_CONFIG.blockedExtensions.includes(normalizedExt)) { throw new SecurityValidationError( `File extension ${normalizedExt} is blocked for security`, 'BLOCKED_EXTENSION' ); } sanitized.push(normalizedExt); } return sanitized; } /** * Validate numeric parameters * @param {any} value - Value to validate * @param {Object} constraints - Validation constraints * @returns {number} Validated number * @throws {SecurityValidationError} If value is invalid */ export function validateNumber(value, constraints = {}) { const { min = 0, max = Number.MAX_SAFE_INTEGER, integer = false } = constraints; if (typeof value !== 'number' && typeof value !== 'string') { throw new SecurityValidationError('Value must be a number', 'INVALID_TYPE'); } const num = Number(value); if (isNaN(num)) { throw new SecurityValidationError('Value is not a valid number', 'INVALID_NUMBER'); } if (integer && !Number.isInteger(num)) { throw new SecurityValidationError('Value must be an integer', 'NOT_INTEGER'); } if (num < min) { throw new SecurityValidationError(`Value must be at least ${min}`, 'VALUE_TOO_LOW'); } if (num > max) { throw new SecurityValidationError(`Value must be at most ${max}`, 'VALUE_TOO_HIGH'); } return num; } /** * Validate and sanitize analysis parameters for DCD/Simian tools * @param {Object} params - Parameters to validate * @param {Object} options - Validation options * @returns {Object} Sanitized parameters * @throws {SecurityValidationError} If any parameter is invalid */ export function validateAnalysisParams(params, options = {}) { const validated = {}; // Validate required path parameter if (!params.path) { throw new SecurityValidationError('Path parameter is required', 'MISSING_REQUIRED_PARAM'); } validated.path = validatePath(params.path, options.pathOptions); // Validate optional parameters if (params.matchLength !== undefined) { validated.matchLength = validateNumber(params.matchLength, { min: 1, max: 1000, integer: true, }); } if (params.threshold !== undefined) { validated.threshold = validateNumber(params.threshold, { min: 2, max: 1000, integer: true, }); } if (params.fuzziness !== undefined) { validated.fuzziness = validateNumber(params.fuzziness, { min: 0, max: 255, integer: true, }); } if (params.extensions !== undefined) { validated.extensions = validateExtensions(params.extensions); } if (params.minOccurrences !== undefined) { validated.minOccurrences = validateNumber(params.minOccurrences, { min: 2, max: 100, integer: true, }); } if (params.complexityThreshold !== undefined) { validated.complexityThreshold = validateNumber(params.complexityThreshold, { min: 1, max: 1000, integer: true, }); } // Validate options object if (params.options !== undefined) { if (typeof params.options !== 'object' || Array.isArray(params.options)) { throw new SecurityValidationError('Options must be an object', 'INVALID_TYPE'); } validated.options = params.options; // Simple object validation for now } logger.debug('Parameters validated successfully'); return validated; } /** * Security configuration for different environments */ export const SECURITY_PROFILES = { development: { allowAbsolutePaths: true, maxFileSize: 500 * 1024 * 1024, // 500MB for dev allowedBasePaths: [], // Allow any path in development }, production: { allowAbsolutePaths: false, maxFileSize: 100 * 1024 * 1024, // 100MB for production allowedBasePaths: [process.cwd()], // Only allow current working directory and subdirs }, strict: { allowAbsolutePaths: false, maxFileSize: 50 * 1024 * 1024, // 50MB for strict mode allowedBasePaths: [process.cwd()], blockedPatterns: [ ...DEFAULT_SECURITY_CONFIG.blockedPatterns, 'node_modules/', '\\.git/', '\\.env', 'secrets/', 'private/', ], }, }; /** * Get security configuration for environment * @param {string} environment - Environment name * @returns {Object} Security configuration */ export function getSecurityConfig(environment = 'development') { // Default to production (strict) for unknown environments for security const profile = SECURITY_PROFILES[environment] || SECURITY_PROFILES.production; return { ...DEFAULT_SECURITY_CONFIG, ...profile }; }