Skip to main content
Glama
query-validator.js12 kB
import { Parser } from 'node-sql-parser'; /** * Enhanced SQL Query Validator using proper SQL parsing * Replaces regex-based validation with comprehensive AST-based analysis */ export class QueryValidator { constructor(config = {}) { this.readOnlyMode = config.readOnlyMode ?? true; this.allowDestructiveOperations = config.allowDestructiveOperations ?? false; this.allowSchemaChanges = config.allowSchemaChanges ?? false; // Initialize SQL parser for multiple databases this.parser = new Parser(); // Define allowed statement types for each security level this.readOnlyStatements = new Set(['select', 'show', 'describe', 'desc', 'explain', 'with']); this.destructiveStatements = new Set([ 'insert', 'update', 'delete', 'truncate', 'merge', 'replace' ]); this.schemaStatements = new Set(['create', 'drop', 'alter', 'rename', 'grant', 'revoke']); // Dangerous functions and procedures that should be blocked this.dangerousFunctions = new Set([ 'xp_cmdshell', 'sp_configure', 'openquery', 'openrowset', 'opendatasource', 'sp_oacreate', 'sp_oamethod', 'bulk' ]); } /** * Validates a SQL query against current security policies * @param {string} query - SQL query to validate * @param {object} context - Additional context (database, tool name, etc.) * @returns {object} Validation result with allowed flag and detailed reason */ validateQuery(query, context = {}) { const trimmedQuery = query.trim(); if (!trimmedQuery) { return { allowed: true, reason: 'Empty query', queryType: 'empty' }; } try { // Parse the SQL query into AST const ast = this.parser.astify(trimmedQuery, { database: 'transactsql' // SQL Server dialect }); // Handle both single statements and arrays of statements const statements = Array.isArray(ast) ? ast : [ast]; // Validate each statement for (let i = 0; i < statements.length; i++) { const stmt = statements[i]; const validation = this.validateStatement(stmt, context, i); if (!validation.allowed) { return { ...validation, statementNumber: i + 1, totalStatements: statements.length }; } } return { allowed: true, reason: 'Query validation passed', statementsCount: statements.length, queryType: this.getQueryType(statements) }; } catch (parseError) { // If parsing fails, fall back to regex validation for better compatibility console.warn(`SQL parsing failed, falling back to regex validation: ${parseError.message}`); return this.fallbackRegexValidation(trimmedQuery, context); } } /** * Validates a single SQL statement * @param {object} stmt - Parsed SQL statement AST * @param {object} context - Validation context * @param {number} index - Statement index in multi-statement query * @returns {object} Validation result */ validateStatement(stmt, _context, _index) { if (!stmt || !stmt.type) { return { allowed: false, reason: 'Invalid or unparseable SQL statement', queryType: 'invalid' }; } const stmtType = stmt.type.toLowerCase(); // Check for dangerous functions first const dangerousFunction = this.checkForDangerousFunctions(stmt); if (dangerousFunction) { return { allowed: false, reason: `Query contains dangerous function '${dangerousFunction}' which is prohibited for security`, queryType: 'dangerous', securityViolation: true }; } // Apply read-only mode restrictions if (this.readOnlyMode) { if (!this.readOnlyStatements.has(stmtType)) { return { allowed: false, reason: `Read-only mode is enabled. Statement type '${stmtType}' is not allowed. Only SELECT queries are permitted. Set SQL_SERVER_READ_ONLY=false to disable.`, queryType: 'non-select', statement: stmtType }; } } // Check destructive operations (if not in read-only mode) if (!this.readOnlyMode && this.destructiveStatements.has(stmtType)) { if (!this.allowDestructiveOperations) { return { allowed: false, reason: `Destructive operation '${stmtType}' is disabled. Set SQL_SERVER_ALLOW_DESTRUCTIVE_OPERATIONS=true to enable.`, queryType: 'destructive', statement: stmtType }; } } // Check schema changes (if not in read-only mode) if (!this.readOnlyMode && this.schemaStatements.has(stmtType)) { if (!this.allowSchemaChanges) { return { allowed: false, reason: `Schema change '${stmtType}' is disabled. Set SQL_SERVER_ALLOW_SCHEMA_CHANGES=true to enable.`, queryType: 'schema', statement: stmtType }; } } // Check for SQL injection patterns in the AST const injectionCheck = this.checkForSqlInjection(stmt); if (!injectionCheck.safe) { return { allowed: false, reason: `Potential SQL injection detected: ${injectionCheck.reason}`, queryType: 'injection', securityViolation: true }; } return { allowed: true, reason: `Statement '${stmtType}' validation passed` }; } /** * Checks for dangerous SQL functions in the AST * @param {object} stmt - SQL statement AST * @returns {string|null} Name of dangerous function found, or null */ checkForDangerousFunctions(stmt) { const checkNode = node => { if (!node || typeof node !== 'object') return null; // Check function calls if (node.type === 'function') { const functionName = node.name?.toLowerCase(); if (functionName && this.dangerousFunctions.has(functionName)) { return functionName; } } // Recursively check all properties for (const key in node) { if (Object.prototype.hasOwnProperty.call(node, key)) { const value = node[key]; if (Array.isArray(value)) { for (const item of value) { const result = checkNode(item); if (result) return result; } } else if (typeof value === 'object') { const result = checkNode(value); if (result) return result; } } } return null; }; return checkNode(stmt); } /** * Advanced SQL injection detection using AST analysis * @param {object} stmt - SQL statement AST * @returns {object} Safety check result */ checkForSqlInjection(stmt) { // This is a simplified implementation - in production, you might want // more sophisticated SQL injection detection const suspiciousPatterns = [ // Look for potential injection in string literals /['"]\s*(;|\s+(OR|AND)\s+)/i, // Union-based injection /\bUNION\s+(ALL\s+)?SELECT\b/i, // Comment-based injection /(-{2}|\/\*|\*\/)/, // System table access /\bsys\./i ]; const checkStringLiterals = node => { if (!node || typeof node !== 'object') return { safe: true }; // Check string values for suspicious patterns if (typeof node.value === 'string') { for (const pattern of suspiciousPatterns) { if (pattern.test(node.value)) { return { safe: false, reason: `Suspicious pattern detected in string literal: '${node.value}'` }; } } } // Recursively check all properties for (const key in node) { if (Object.prototype.hasOwnProperty.call(node, key)) { const value = node[key]; if (Array.isArray(value)) { for (const item of value) { const result = checkStringLiterals(item); if (!result.safe) return result; } } else if (typeof value === 'object') { const result = checkStringLiterals(value); if (!result.safe) return result; } } } return { safe: true }; }; return checkStringLiterals(stmt); } /** * Fallback regex validation for when SQL parsing fails * @param {string} query - SQL query string * @param {object} context - Validation context * @returns {object} Validation result */ fallbackRegexValidation(query, _context) { // Use the original regex patterns as fallback const destructivePatterns = [ /^\s*(DELETE|UPDATE|INSERT|TRUNCATE)\s+/i, /^\s*EXEC(UTE)?\s+/i, /^\s*CALL\s+/i, /;\s*(DELETE|UPDATE|INSERT|TRUNCATE)\s+/i ]; const schemaChangePatterns = [ /^\s*(CREATE|DROP|ALTER)\s+/i, /^\s*(GRANT|REVOKE)\s+/i, /;\s*(CREATE|DROP|ALTER|GRANT|REVOKE)\s+/i ]; const readOnlyPatterns = [ /^\s*SELECT\s+/i, /^\s*SHOW\s+/i, /^\s*DESCRIBE\s+/i, /^\s*DESC\s+/i, /^\s*EXPLAIN\s+/i, /^\s*WITH\s+[\s\S]*?\bSELECT\s+/i ]; // Apply read-only mode restrictions if (this.readOnlyMode) { const isReadOnlyQuery = readOnlyPatterns.some(pattern => pattern.test(query)); if (!isReadOnlyQuery) { return { allowed: false, reason: 'Read-only mode is enabled. Only SELECT queries are allowed. Set SQL_SERVER_READ_ONLY=false to disable.', queryType: 'non-select', fallback: true }; } } // Check for destructive operations if (!this.readOnlyMode) { const hasDestructiveOps = destructivePatterns.some(pattern => pattern.test(query)); if (hasDestructiveOps && !this.allowDestructiveOperations) { return { allowed: false, reason: 'Destructive operations (INSERT/UPDATE/DELETE) are disabled. Set SQL_SERVER_ALLOW_DESTRUCTIVE_OPERATIONS=true to enable.', queryType: 'destructive', fallback: true }; } } // Check for schema changes if (!this.readOnlyMode) { const hasSchemaChanges = schemaChangePatterns.some(pattern => pattern.test(query)); if (hasSchemaChanges && !this.allowSchemaChanges) { return { allowed: false, reason: 'Schema changes (CREATE/DROP/ALTER) are disabled. Set SQL_SERVER_ALLOW_SCHEMA_CHANGES=true to enable.', queryType: 'schema', fallback: true }; } } return { allowed: true, reason: 'Fallback regex validation passed', fallback: true }; } /** * Determines the overall query type from parsed statements * @param {array} statements - Array of parsed statements * @returns {string} Query type classification */ getQueryType(statements) { if (statements.length === 0) return 'empty'; if (statements.length > 1) return 'multi-statement'; const stmt = statements[0]; return stmt.type?.toLowerCase() || 'unknown'; } /** * Updates security configuration * @param {object} newConfig - New security configuration */ updateConfig(newConfig) { if (Object.prototype.hasOwnProperty.call(newConfig, 'readOnlyMode')) { this.readOnlyMode = newConfig.readOnlyMode; } if (Object.prototype.hasOwnProperty.call(newConfig, 'allowDestructiveOperations')) { this.allowDestructiveOperations = newConfig.allowDestructiveOperations; } if (Object.prototype.hasOwnProperty.call(newConfig, 'allowSchemaChanges')) { this.allowSchemaChanges = newConfig.allowSchemaChanges; } } /** * Gets current security configuration * @returns {object} Current security settings */ getConfig() { return { readOnlyMode: this.readOnlyMode, allowDestructiveOperations: this.allowDestructiveOperations, allowSchemaChanges: this.allowSchemaChanges }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/egarcia74/warp-sql-server-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server