UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

275 lines (231 loc) 7.52 kB
/** * Input Validators with Security Best Practices * * OWASP-compliant validators for all wizard inputs * Protects against: command injection, path traversal, XSS, buffer overflow * * @see docs/stories/v4.0.4/sprint-1/story-1.2-interactive-wizard-foundation.md * @module wizard/validators */ const path = require('path'); /** * Maximum input lengths to prevent buffer overflow */ const INPUT_LIMITS = { projectName: 100, path: 255, generic: 500, }; /** * Allowed project types (whitelist) */ const ALLOWED_PROJECT_TYPES = ['greenfield', 'brownfield']; /** * Shell-special characters that need escaping/rejection */ const SHELL_SPECIAL_CHARS = /[;&|$`()\\<>\n]/; /** * Validate project type against whitelist * * @param {string} input - User input * @returns {boolean|string} True if valid, error message if invalid */ function validateProjectType(input) { if (!input || typeof input !== 'string') { return 'Project type is required'; } const normalized = input.toLowerCase().trim(); if (!ALLOWED_PROJECT_TYPES.includes(normalized)) { return `Invalid project type. Must be one of: ${ALLOWED_PROJECT_TYPES.join(', ')}`; } return true; } /** * Validate and sanitize path input * Prevents path traversal attacks (../../../etc/passwd) * * Security policy: * - Cross-drive access is DISALLOWED for security (e.g., C:\ vs D:\ on Windows) * - Path must resolve within baseDir (no up-level traversal) * - Uses path.relative() and root comparison for robust validation across platforms * * @param {string} input - Path input * @param {string} baseDir - Base directory (default: process.cwd()) * @returns {boolean|string} True if valid, error message if invalid */ function validatePath(input, baseDir = process.cwd()) { if (!input || typeof input !== 'string') { return 'Path is required'; } // Check length if (input.length > INPUT_LIMITS.path) { return `Path too long (max ${INPUT_LIMITS.path} characters)`; } // Reject shell-special characters in paths if (SHELL_SPECIAL_CHARS.test(input)) { return 'Path contains invalid characters'; } // Resolve and verify path is within base directory // Normalize both paths to handle trailing slashes and relative segments const normalizedBaseDir = path.resolve(baseDir); const resolved = path.resolve(normalizedBaseDir, input); // On Windows, check for cross-drive access (e.g., C:\ vs D:\) // Cross-drive targets are disallowed for security - path must stay within same root const baseRoot = path.parse(normalizedBaseDir).root; const resolvedRoot = path.parse(resolved).root; if (baseRoot !== resolvedRoot) { return 'Path must be within project directory (cross-drive access not allowed)'; } // Use path.relative to detect traversal attempts // If resolved is within baseDir, relative path won't start with '..' const relativePath = path.relative(normalizedBaseDir, resolved); // Check for up-level traversal indicators // Empty string means paths are identical, which is valid if (relativePath && (relativePath.startsWith('..') || relativePath.includes('..'))) { return 'Path must be within project directory (path traversal detected)'; } return true; } /** * Sanitize text input * Removes/escapes shell-special characters * * @param {string} input - Text input * @param {number} maxLength - Maximum length (default: INPUT_LIMITS.generic) * @returns {boolean|string} True if valid, error message if invalid */ function validateTextInput(input, maxLength = INPUT_LIMITS.generic) { if (!input || typeof input !== 'string') { return 'Input is required'; } // Trim whitespace const trimmed = input.trim(); // Check length if (trimmed.length === 0) { return 'Input cannot be empty'; } if (trimmed.length > maxLength) { return `Input too long (max ${maxLength} characters)`; } // Reject shell-special characters if (SHELL_SPECIAL_CHARS.test(trimmed)) { return 'Input contains invalid characters (shell-special characters not allowed)'; } // Check for potential command injection patterns const injectionPatterns = [ /\$\(/, // $(command) /`[^`]+`/, // `command` /&&/, // command chaining /\|\|/, // command chaining /<script/i, // XSS-style /<img/i, // XSS-style ]; for (const pattern of injectionPatterns) { if (pattern.test(trimmed)) { return 'Input contains potentially malicious patterns'; } } return true; } /** * Validate project name * Stricter than generic text - only alphanumeric, dash, underscore * * @param {string} input - Project name * @returns {boolean|string} True if valid, error message if invalid */ function validateProjectName(input) { if (!input || typeof input !== 'string') { return 'Project name is required'; } const trimmed = input.trim(); // Check length if (trimmed.length === 0) { return 'Project name cannot be empty'; } if (trimmed.length > INPUT_LIMITS.projectName) { return `Project name too long (max ${INPUT_LIMITS.projectName} characters)`; } // Only allow alphanumeric, dash, underscore if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) { return 'Project name can only contain letters, numbers, dashes, and underscores'; } // Must start with letter or number if (!/^[a-zA-Z0-9]/.test(trimmed)) { return 'Project name must start with a letter or number'; } return true; } /** * Create inquirer-compatible validator function * Converts validator return to inquirer format * * @param {Function} validatorFn - Validator function * @returns {Function} Inquirer-compatible validator */ function createInquirerValidator(validatorFn) { return (input) => { const result = validatorFn(input); return result === true ? true : result; }; } /** * Sanitize string by escaping shell-special characters * Use ONLY if you absolutely need to preserve special chars * Prefer validation/rejection instead * * @param {string} input - String to sanitize * @returns {string} Sanitized string */ function sanitizeShellInput(input) { if (typeof input !== 'string') { return ''; } // Escape shell-special characters const sanitized = input .replace(/\\/g, '\\\\') .replace(/'/g, "\\'") .replace(/"/g, '\\"') .replace(/`/g, '\\`') .replace(/\$/g, '\\$') .replace(/;/g, '\\;') .replace(/&/g, '\\&') .replace(/\|/g, '\\|') .replace(/</g, '\\<') .replace(/>/g, '\\>') .replace(/\(/g, '\\(') .replace(/\)/g, '\\)') .replace(/\n/g, '\\n'); return sanitized; } /** * Validate list selection (for IDE, MCP, etc.) * * @param {string} input - Selected value * @param {string[]} allowedValues - Whitelist of allowed values * @returns {boolean|string} True if valid, error message if invalid */ function validateListSelection(input, allowedValues) { if (!input || typeof input !== 'string') { return 'Selection is required'; } if (!allowedValues.includes(input)) { return `Invalid selection. Must be one of: ${allowedValues.join(', ')}`; } return true; } module.exports = { // Core validators validateProjectType, validatePath, validateTextInput, validateProjectName, validateListSelection, // Utility functions createInquirerValidator, sanitizeShellInput, // Constants (export for testing) INPUT_LIMITS, ALLOWED_PROJECT_TYPES, SHELL_SPECIAL_CHARS, };