/** * SQL Sanitizer - The Last Line of Defense * Validates raw SQL before execution to prevent catastrophic accidents */ export interface SanitizationResult { safe: boolean; warnings: string[]; blocked?: string; } // Dangerous patterns that should NEVER be allowed const BLOCKED_PATTERNS = [ /DROP\s+DATABASE/i, /DROP\s+SCHEMA/i, /ALTER\s+USER/i, /ALTER\s+ROLE/i, /CREATE\s+USER/i, /CREATE\s+ROLE/i, /GRANT\s+.*\s+TO/i, /REVOKE\s+.*\s+FROM/i, ]; // Patterns that require special attention const WARNING_PATTERNS = [ { pattern: /TRUNCATE/i, message: 'TRUNCATE detected - use with caution' }, { pattern: /DROP\s+TABLE/i, message: 'DROP TABLE detected - irreversible' }, { pattern: /DELETE\s+FROM\s+\w+\s*;/i, message: 'DELETE without WHERE clause - will delete ALL rows' }, { pattern: /UPDATE\s+\w+\s+SET\s+.*\s*;/i, message: 'UPDATE without WHERE clause - will update ALL rows' }, ]; /** * Validate table name is safe (alphanumeric + underscores only) */ export function validateTableName(tableName: string): boolean { return /^[a-zA-Z0-9_]+$/.test(tableName); } /** * Sanitize raw SQL before execution */ export function sanitizeSQL(sql: string): SanitizationResult { const trimmed = sql.trim(); const warnings: string[] = []; // Check for blocked patterns for (const pattern of BLOCKED_PATTERNS) { if (pattern.test(trimmed)) { return { safe: false, warnings: [], blocked: `Blocked dangerous command: ${pattern.source}` }; } } // Check for warning patterns for (const { pattern, message } of WARNING_PATTERNS) { if (pattern.test(trimmed)) { warnings.push(message); } } // Validate table names in common operations const tableMatches = trimmed.match(/(?:FROM|INTO|UPDATE|TRUNCATE|DROP TABLE)\s+([a-zA-Z0-9_]+)/gi); if (tableMatches) { for (const match of tableMatches) { const tableName = match.split(/\s+/).pop() || ''; if (!validateTableName(tableName)) { return { safe: false, warnings: [], blocked: `Invalid table name: ${tableName}` }; } } } return { safe: true, warnings }; } /** * Extract table names from SQL query */ export function extractTableNames(sql: string): string[] { const matches = sql.match(/(?:FROM|INTO|UPDATE|TRUNCATE|DROP TABLE)\s+([a-zA-Z0-9_]+)/gi); if (!matches) return []; return matches.map(m => m.split(/\s+/).pop() || '').filter(Boolean); } /** * Check if SQL contains WHERE clause */ export function hasWhereClause(sql: string): boolean { return /WHERE\s+/i.test(sql); } /** * More permissive check for maintenance window * Used when run_mechanic flag is set */ export function sanitizeSQLForMaintenance(sql: string): SanitizationResult { const trimmed = sql.trim(); const warnings: string[] = []; // Still block the most dangerous commands const criticalPatterns = [ /DROP\s+DATABASE/i, /DROP\s+SCHEMA/i, /ALTER\s+USER/i, /CREATE\s+USER/i, ]; for (const pattern of criticalPatterns) { if (pattern.test(trimmed)) { return { safe: false, warnings: [], blocked: `Blocked critical command even in maintenance mode: ${pattern.source}` }; } } // Allow TRUNCATE and DROP TABLE in maintenance mode warnings.push('Maintenance mode: dangerous operations allowed'); return { safe: true, warnings }; }