import {Request} from '@loopback/rest'; import {DEFAULT_SQL_INJECTION_CONFIG} from '../constants/security-defaults'; import {SqlInjectionConfig} from '../types'; import {SecurityError} from '../utils/security-error'; import {SecurityLogger} from '../utils/security-logger'; import {InputValidator} from '../validators'; /** * SQL injection detector that scans for common SQL injection patterns */ export class SqlInjectionDetector { private readonly defaultPatterns: RegExp[] = [ // Common SQL keywords in injection contexts (more specific) /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE|UNION|SCRIPT)\b.*\b(FROM|WHERE|INTO|TABLE|DATABASE)\b)/gi, // SQL keywords followed by dangerous operations /(DROP\s+(TABLE|DATABASE|SCHEMA|INDEX|VIEW))/gi, /(DELETE\s+FROM\s+)/gi, /(INSERT\s+INTO\s+)/gi, // SQL injection attempts with logical operators (more context-aware) /(\b(OR|AND)\b\s*['"]?\d+['"]?\s*=\s*['"]?\d+['"]?(\s|;|--|\/\*|$))/gi, // Comment-based injections with SQL context (more specific) /(;[\s]*--)|(\/\*.*\*\/)|(--[\s]*$)/gi, // Enhanced SQL comment-based injection patterns /(['"`]\s*--)/gi, // Quote followed by SQL comment /(\bOR\b.*--)|(\bAND\b.*--)/gi, // Logical operators with comment /(=.*--)/gi, // Equals with comment (common in WHERE clauses) /(1=1\s*--)/gi, // Tautology with comment /(1=0\s*--)/gi, // Always false with comment /(UNION.*--)/gi, // UNION with comment /(SELECT.*--)/gi, // SELECT with comment /(INSERT.*--)/gi, // INSERT with comment /(UPDATE.*--)/gi, // UPDATE with comment /(DELETE.*--)/gi, // DELETE with comment /(DROP.*--)/gi, // DROP with comment // URL-encoded injection attempts in SQL context /(%27|%22|%3B|%2D%2D).*\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION)\b/gi, // Single/double quotes followed by SQL operations /('+|"+)\s*(OR|AND|UNION|SELECT|INSERT|UPDATE|DELETE)\s/gi, // Union-based injections /(\b(UNION)\b\s*(ALL\s+)?\b(SELECT)\b)/gi, // Tautology-based injections (more specific) /(OR\s+['"]?1['"]?\s*=\s*['"]?1['"]?|AND\s+['"]?1['"]?\s*=\s*['"]?1['"]?)/gi, /('+\s*OR\s*'+\s*=\s*'+|"+\s*OR\s*"+\s*=\s*"+)/gi, /(['"]?\s*OR\s*['"]?1['"]?\s*=\s*['"]?1['"]?)/gi, // SQL functions that might be misused /(\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b)/gi, // Database-specific injection patterns /(INFORMATION_SCHEMA|SYSOBJECTS|SYSCOLUMNS|@@VERSION|@@SERVERNAME)/gi, // Hex-encoded SQL with context /(0x[0-9a-fA-F]+.*\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION)\b)/gi, // Time-based blind injection patterns /(SLEEP\s*\(|WAITFOR\s+DELAY|BENCHMARK\s*\()/gi, // Boolean-based blind injection patterns /(\bIF\s*\(.*,.*,.*\)|\bCASE\s+WHEN\b)/gi, // Stored procedure execution attempts /(EXEC\s*\(|EXECUTE\s*\(|SP_)/gi, // Classic injection patterns that should always be caught /(';[\s]*DROP[\s]+TABLE)/gi, /(';[\s]*DELETE[\s]+FROM)/gi, /(OR[\s]+1[\s]*=[\s]*1)/gi, /(AND[\s]+1[\s]*=[\s]*1)/gi ]; private patterns: RegExp[]; private whitelistedQueries: Set; private skipFields: string[]; constructor( private config?: SqlInjectionConfig, private logger?: SecurityLogger, skipFields?: string[] ) { // Merge with default configuration this.config = { ...DEFAULT_SQL_INJECTION_CONFIG, ...config }; // Combine default patterns with custom patterns this.patterns = [ ...this.defaultPatterns, ...(this.config?.customPatterns ?? []) ]; // Convert whitelist to Set for faster lookups this.whitelistedQueries = new Set(this.config?.whitelistQueries ?? []); // Set skip fields this.skipFields = skipFields ?? []; this.logger?.logSecurityEvent({}, { timestamp: new Date().toISOString(), eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', metadata: { component: 'SqlInjectionDetector', patternsLoaded: this.patterns.length, whitelistedQueries: this.whitelistedQueries.size } }, 'SqlInjectionDetector', 'constructor'); } /** * Detect SQL injection attempts in the request */ async detectSqlInjection(req: Request): Promise { if (this.config?.enableProtection === false) { return; } try { // Check query parameters if (req.query) { await this.scanObject(req.query, 'query parameters', req); } // Check request body if (req.body) { await this.scanObject(req.body, 'request body', req); } // Check URL parameters if (req.params) { await this.scanObject(req.params, 'URL parameters', req); } // Check headers for SQL injection (always enabled for security) if (req.headers) { await this.scanHeaders(req.headers, req); } } catch (error) { if (error instanceof SecurityError) { throw error; } // Log unexpected errors this.logger?.logSecurityViolation(req, error instanceof Error ? error : String(error), { component: 'SqlInjectionDetector', method: 'detectSqlInjection' }, 'SqlInjectionDetector', 'detectSqlInjection'); throw SecurityError.fromError(error); } } /** * Check if a field should be skipped from SQL injection detection */ private shouldSkipField(fieldName: string): boolean { if (!this.skipFields || this.skipFields.length === 0) { return false; } const lowerFieldName = fieldName.toLowerCase(); return this.skipFields.some(skipField => lowerFieldName.includes(skipField.toLowerCase()) ); } /** * Scan object recursively for SQL injection patterns */ private async scanObject( obj: Record, location: string, req: Request, path = '' ): Promise { if (!obj || typeof obj !== 'object') { return; } const input = new InputValidator() for (const [key, value] of Object.entries(obj)) { const currentPath = path ? `${path}.${key}` : key; // Skip SQL injection detection for fields that are in the skip list if (this.shouldSkipField(key)) { this.logger?.logSecurityEvent(req, { timestamp: new Date().toISOString(), eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', metadata: { component: 'SqlInjectionDetector', action: 'FIELD_SKIPPED', fieldName: key, location: `${location}.${currentPath}`, reason: 'Field in skip list' } }); continue; } const isPhone = input.isPhoneField(key); if (isPhone && value && input.isValidPhoneNumber(value as string)) { continue; // Skip valid phone numbers } if (typeof value === 'string') { // Special handling for URL parameters and path parameters if (location.includes('URL parameters') || location.includes('query parameters')) { await this.scanUrlParameter(value, `${location}.${currentPath}`, req); } else { await this.scanStringValue(value, `${location}.${currentPath}`, req); } } else if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { if (typeof value[i] === 'string') { if (location.includes('URL parameters') || location.includes('query parameters')) { await this.scanUrlParameter(value[i] as string, `${location}.${currentPath}[${i}]`, req); } else { await this.scanStringValue(value[i] as string, `${location}.${currentPath}[${i}]`, req); } } else if (typeof value[i] === 'object' && value[i] !== null) { await this.scanObject(value[i] as Record, location, req, `${currentPath}[${i}]`); } } } else if (typeof value === 'object' && value !== null) { await this.scanObject(value as Record, location, req, currentPath); } } } /** * Scan URL parameters with more lenient patterns to allow legitimate URL characters */ private async scanUrlParameter(value: string, location: string, req: Request): Promise { // Skip empty strings if (!value.trim()) { return; } // Check if this parameter is whitelisted if (this.isWhitelisted(value)) { return; } // For URL parameters, use more restrictive patterns that focus on clear SQL injection attempts const urlParameterSqlPatterns: RegExp[] = [ // Clear SQL injection attempts with semicolons and SQL keywords /;\s*(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER)\s+/gi, // Standalone SQL statements (even without semicolons) /\b(DELETE\s+\*?\s*FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DROP\s+TABLE|CREATE\s+TABLE)\b/gi, // Classic injection patterns with quotes and SQL operations /['"`]\s*(OR|AND|UNION)\s+/gi, /'[^']*'\s*;\s*(DROP|DELETE|INSERT|UPDATE|SELECT)/gi, /"\s*;\s*(DROP|DELETE|INSERT|UPDATE|SELECT)/gi, // Union-based injection (clear attack pattern) /\bUNION\s+(ALL\s+)?SELECT\b/gi, // Obvious tautology attacks with quotes /'[^']*'\s*OR\s*'[^']*'\s*=\s*'[^']*'/gi, /"[^"]*"\s*OR\s*"[^"]*"\s*=\s*"[^"]*"/gi, // Database information disclosure attempts /\b(INFORMATION_SCHEMA|SYSOBJECTS|SYSCOLUMNS)\b/gi, // Time-based injection patterns /\b(SLEEP\s*\(|WAITFOR\s+DELAY|BENCHMARK\s*\()/gi, // SQL comments with injection context /(--|\/\*|\*\/).*\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION)\b/gi, // Common SQL patterns that should not appear in URL parameters /\b(SELECT\s+\*\s+FROM|DELETE\s+FROM)\b/gi, ]; // Decode URL-encoded values for better detection const decodedValue = this.decodeValue(value); // Check against URL parameter specific patterns for (const pattern of urlParameterSqlPatterns) { pattern.lastIndex = 0; if (pattern.test(value) || pattern.test(decodedValue)) { this.logAndThrowSqlInjection(value, location, req, pattern); return; } } } /** * Scan headers for SQL injection attempts */ private async scanHeaders(headers: Record, req: Request): Promise { // Headers that should never be scanned due to their nature const headersToCompletelySkip = new Set([ 'content-length', // Always numeric 'connection', // Connection types only 'upgrade-insecure-requests', // Browser security (0 or 1) 'sec-fetch-site', // Browser security (controlled values) 'sec-fetch-mode', // Browser security (controlled values) 'sec-fetch-dest', // Browser security (controlled values) 'sec-fetch-user', // Browser security (controlled values) 'sec-ch-ua', // Browser client hints 'sec-ch-ua-mobile', // Browser client hints 'sec-ch-ua-platform', // Browser client hints 'te', // Transfer encoding 'upgrade', // Protocol upgrade 'via', // Proxy information 'x-forwarded-for', // Proxy forwarding 'x-forwarded-host', // Proxy forwarding 'x-forwarded-proto', // Proxy forwarding 'x-real-ip', // Real IP from proxy 'x-requested-with', // AJAX indicator 'access-control-request-method', // CORS preflight 'access-control-request-headers', // CORS preflight 'origin', // Request origin 'dnt', // Do not track 'pragma', // HTTP 1.0 cache control 'expires', // Cache expiration 'if-modified-since', // Conditional request 'if-none-match', // Conditional request 'range', // Partial content request ]); // Headers that need careful validation (check for obvious SQL but allow legitimate content) const headersNeedingCarefulValidation = new Set([ 'user-agent', // Can contain legitimate special chars but shouldn't have obvious SQL 'accept', // MIME types with parameters 'accept-language', // Language codes 'accept-encoding', // Encoding types 'content-type', // MIME types with parameters 'cookie', // Session data 'cache-control', // Cache directives 'host', // Hostname (validated elsewhere but can be manipulated) 'referer', // Referrer URL 'authorization', // Auth tokens (but still check for obvious SQL) ]); for (const [headerName, headerValue] of Object.entries(headers)) { const normalizedHeaderName = headerName.toLowerCase(); // Skip headers that should never be checked if (headersToCompletelySkip.has(normalizedHeaderName)) { continue; } if (typeof headerValue === 'string') { // For headers that need careful validation, use more restrictive patterns if (headersNeedingCarefulValidation.has(normalizedHeaderName)) { await this.scanHeaderValueCarefully(headerValue, `header.${headerName}`, req, normalizedHeaderName); } else { // For custom headers and other headers, use full validation await this.scanStringValue(headerValue, `header.${headerName}`, req); } } } } /** * Scan header values with more restrictive patterns to avoid false positives * while still catching obvious SQL injection attempts */ private async scanHeaderValueCarefully(value: string, location: string, req: Request, headerName: string): Promise { // Skip empty strings if (!value.trim()) { return; } // For certain headers, use more restrictive validation patterns // These patterns focus on obvious SQL injection attempts while allowing legitimate content const highConfidenceSqlPatterns: RegExp[] = [ // Clear SQL injection attempts with semicolons and SQL keywords /;\s*(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER|SELECT)\s+/gi, // Standalone SQL statements that should never appear in headers /\b(SELECT\s+\*?\s*FROM|DELETE\s+\*?\s*FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DROP\s+TABLE|CREATE\s+TABLE)\b/gi, // Classic injection patterns with quotes and comments /['"`]\s*;\s*(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER|SELECT)/gi, /'[^']*'\s*;\s*--/gi, /"\s*;\s*--/gi, // Union-based injection (clear attack pattern) /\bUNION\s+(ALL\s+)?SELECT\b/gi, // Obvious tautology attacks /\bOR\s+1\s*=\s*1\s*(--|\s*$)/gi, /\bAND\s+1\s*=\s*2\s*(--|\s*$)/gi, /'[^']*'\s*OR\s+'[^']*'\s*=\s*'[^']*'/gi, // Database information disclosure attempts /\b(INFORMATION_SCHEMA|SYSOBJECTS|SYSCOLUMNS)\b/gi, // Time-based injection patterns /\b(SLEEP\s*\(|WAITFOR\s+DELAY|BENCHMARK\s*\()/gi, // SQL comments that indicate injection attempts /(--|\/\*|\*\/).*\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION)\b/gi, // Clear SQL patterns that should not appear in any header /\b(SELECT\s+.*\s+FROM\s+|DELETE\s+FROM\s+|INSERT\s+INTO\s+)/gi, ]; // Special handling for specific headers if (headerName === 'user-agent') { // For user-agent, only check for the most obvious SQL injection patterns // Allow legitimate browser strings that might contain SELECT, etc. const userAgentSqlPatterns = [ /;\s*DROP\s+TABLE/gi, /;\s*DELETE\s+FROM/gi, /'\s*OR\s+1\s*=\s*1/gi, /UNION\s+SELECT/gi, ]; for (const pattern of userAgentSqlPatterns) { pattern.lastIndex = 0; if (pattern.test(value)) { this.logAndThrowSqlInjection(value, location, req, pattern); return; } } return; } if (headerName === 'cookie') { // For cookies, be more careful but still check for obvious attacks // Skip if it looks like a legitimate session token or UUID if (/^[a-zA-Z0-9+/=_-]+$/.test(value) && value.length > 20) { return; // Likely a legitimate session token } } if (headerName === 'authorization') { // For authorization headers, be very lenient - only catch the most obvious attacks // JWT tokens, API keys, etc. can contain many special characters const authSqlPatterns = [ /;\s*DROP\s+TABLE/gi, /;\s*DELETE\s+FROM/gi, /UNION\s+SELECT.*FROM/gi, ]; for (const pattern of authSqlPatterns) { pattern.lastIndex = 0; if (pattern.test(value)) { this.logAndThrowSqlInjection(value, location, req, pattern); return; } } return; } if (headerName === 'referer' || headerName === 'referrer') { // For referrer URLs, only check if it contains obvious SQL injection // URLs can contain many special characters legitimately if (!/https?:\/\//.test(value)) { // If it doesn't look like a URL, apply more strict validation for (const pattern of highConfidenceSqlPatterns) { pattern.lastIndex = 0; if (pattern.test(value)) { this.logAndThrowSqlInjection(value, location, req, pattern); return; } } } return; } // Check against high-confidence patterns for other carefully validated headers for (const pattern of highConfidenceSqlPatterns) { pattern.lastIndex = 0; if (pattern.test(value)) { this.logAndThrowSqlInjection(value, location, req, pattern); return; } } } /** * Helper method to log and throw SQL injection detection */ private logAndThrowSqlInjection(value: string, location: string, req: Request, pattern: RegExp): void { // Log the detection this.logger?.logMaliciousActivity(req, 'SQL_INJECTION_ATTEMPT', `Pattern detected in ${location}`, { pattern: pattern.toString(), value: this.sanitizeLogValue(value), location }, 'SqlInjectionDetector', 'scanValue'); // Block the request if strict mode is enabled or if it's a high-risk pattern if (this.config?.strictMode || this.isHighRiskPattern(pattern)) { throw SecurityError.sqlInjectionDetected(location, pattern.toString(), { originalValue: this.sanitizeLogValue(value), patternMatched: pattern.toString() }); } // In non-strict mode, just log and continue for some patterns if (this.config?.blockSuspiciousPatterns !== false) { throw SecurityError.sqlInjectionDetected(location, pattern.toString()); } } /** * Scan individual string value for SQL injection patterns */ private async scanStringValue(value: string, location: string, req: Request): Promise { // Skip empty strings if (!value.trim()) { return; } // Check if this query is whitelisted if (this.isWhitelisted(value)) { return; } // Decode URL-encoded values for better detection const decodedValue = this.decodeValue(value); // Check against all patterns for (const pattern of this.patterns) { // Reset regex lastIndex to ensure consistent matching pattern.lastIndex = 0; if (pattern.test(value) || (decodedValue !== value && pattern.test(decodedValue))) { // Log the detection this.logger?.logMaliciousActivity(req, 'SQL_INJECTION_ATTEMPT', `Pattern detected in ${location}`, { pattern: pattern.toString(), value: this.sanitizeLogValue(value), location, decodedValue: value !== decodedValue ? this.sanitizeLogValue(decodedValue) : undefined }, 'SqlInjectionDetector', 'scanValue'); // Block the request if strict mode is enabled or if it's a high-risk pattern if (this.config?.strictMode || this.isHighRiskPattern(pattern)) { throw SecurityError.sqlInjectionDetected(location, pattern.toString(), { originalValue: this.sanitizeLogValue(value), decodedValue: value !== decodedValue ? this.sanitizeLogValue(decodedValue) : undefined, patternMatched: pattern.toString() }); } // In non-strict mode, just log and continue for some patterns if (this.config?.blockSuspiciousPatterns !== false) { throw SecurityError.sqlInjectionDetected(location, pattern.toString()); } } } } /** * Check if a query is whitelisted */ private isWhitelisted(value: string): boolean { const trimmedValue = value.toLowerCase().trim(); // Check exact whitelist matches first if (this.whitelistedQueries.has(trimmedValue)) { return true; } // Check for common legitimate patterns that shouldn't be flagged as SQL injection const legitimatePatterns = [ // Sorting patterns /^[a-zA-Z_][a-zA-Z0-9_]*\/(asc|desc)$/i, // Boolean values /^(true|false)$/i, // Numbers (including negative and decimals) /^-?\d+(\.\d+)?$/, // Date/time patterns /^\d{4}-\d{2}-\d{2}[\s\+]\d{2}:\d{2}:\d{2}$/, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, /^\d{4}-\d{2}-\d{2}$/, // Array notation (for query params like ENV[]) /^[a-zA-Z_][a-zA-Z0-9_]*\[\]$/, // Common status/enum values (all uppercase) /^[A-Z_]+$/, // UUID patterns /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, // Email addresses (basic pattern) /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, // Phone numbers with various formats /^[\+]?[\d\s\-\(\)\.]{7,15}$/, // Common file extensions and MIME types /^[a-zA-Z0-9]+\/[a-zA-Z0-9\-\+\.]+$/, // Common User-Agent patterns (browsers, mobile apps, etc.) /^Mozilla\/\d+\.\d+\s+\([^)]+\)\s+AppleWebKit\/[\d.]+\s+\([^)]+\)\s+Chrome\/[\d.]+\s+Safari\/[\d.]+$/i, // Chrome /^Mozilla\/\d+\.\d+\s+\([^)]+\)\s+AppleWebKit\/[\d.]+\s+\([^)]+\)\s+Version\/[\d.]+\s+Safari\/[\d.]+$/i, // Safari /^Mozilla\/\d+\.\d+\s+\([^)]+\)\s+Gecko\/\d+\s+Firefox\/[\d.]+$/i, // Firefox /^Mozilla\/\d+\.\d+\s+\([^)]+\)\s+AppleWebKit\/[\d.]+\s+\([^)]+\)\s+Edge\/[\d.]+$/i, // Edge ]; // Check if value matches any legitimate pattern for (const pattern of legitimatePatterns) { if (pattern.test(value)) { return true; } } // Special check for URLs and general text with special chars (allow & and %27 if no SQL keywords) if (/^[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%\s]+$/.test(value) && value.length < 1000 && !/\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|OR|AND)\b/gi.test(value) && !/(;[\s]*--)|(\/\*.*\*\/)/gi.test(value) && !/('+\s*OR\s*'+\s*=\s*'+)/gi.test(value) && !/("+\s*OR\s*"+\s*=\s*"+)/gi.test(value) && !/OR\s*['"]?\d*['"]?\s*=\s*['"]?\d*['"]?/gi.test(value)) { return true; } // Special check for Base64 encoded data - only if it doesn't decode to SQL if (/^[A-Za-z0-9+/]+=*$/.test(value) && value.length > 20 && !this.mightBeSqlInjectionBase64(value)) { return true; } return false; } /** * Decode URL-encoded and other encoded values for better detection */ private decodeValue(value: string): string { try { // First, handle + as spaces (URL form encoding) let decoded = value.replace(/\+/g, ' '); // Then URL decode decoded = decodeURIComponent(decoded); // HTML entity decode (basic) decoded = decoded .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(///g, '/') .replace(/&/g, '&'); // Only decode base64 if it's actually an injection attempt // Don't decode legitimate base64 data like tokens or file uploads if (this.isBase64(decoded) && this.mightBeSqlInjectionBase64(decoded)) { try { const base64Decoded = Buffer.from(decoded, 'base64').toString('utf-8'); return base64Decoded; } catch { // If base64 decode fails, return the original decoded value } } return decoded; } catch { // If decoding fails, return original value return value; } } /** * Check if base64 string might contain SQL injection */ private mightBeSqlInjectionBase64(str: string): boolean { try { const decoded = Buffer.from(str, 'base64').toString('utf-8'); // Only consider it potential SQL injection if it contains SQL keywords return /\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|OR|AND)\b/gi.test(decoded); } catch { return false; } } /** * Check if string looks like base64 */ private isBase64(str: string): boolean { // Basic base64 pattern check const base64Pattern = /^[A-Za-z0-9+/]*={0,2}$/; return str.length > 10 && str.length % 4 === 0 && base64Pattern.test(str); } /** * Determine if pattern represents high-risk SQL injection attempt */ private isHighRiskPattern(pattern: RegExp): boolean { const highRiskPatterns = [ /DROP|DELETE|TRUNCATE/gi, /INFORMATION_SCHEMA|SYSOBJECTS/gi, /LOAD_FILE|INTO\s+OUTFILE/gi, /UNION.*SELECT/gi ]; return highRiskPatterns.some(highRiskPattern => pattern.toString() === highRiskPattern.toString() ); } /** * Sanitize value for logging (remove sensitive content) */ private sanitizeLogValue(value: string): string { // Truncate long values if (value.length > 200) { return value.substring(0, 200) + '...'; } // Mask potential passwords or sensitive data return value.replace(/(password|pwd|secret|key|token)=([^&\s]+)/gi, '$1=***MASKED***'); } /** * Add custom pattern to detection */ addCustomPattern(pattern: RegExp): void { this.patterns.push(pattern); this.logger?.logSecurityEvent({}, { timestamp: new Date().toISOString(), eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', metadata: { component: 'SqlInjectionDetector', action: 'CUSTOM_PATTERN_ADDED', pattern: pattern.toString() } }); } /** * Add query to whitelist */ addWhitelistedQuery(query: string): void { this.whitelistedQueries.add(query.toLowerCase().trim()); this.logger?.logSecurityEvent({}, { timestamp: new Date().toISOString(), eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', metadata: { component: 'SqlInjectionDetector', action: 'QUERY_WHITELISTED', queryHash: this.hashString(query) // Don't log the actual query for security } }); } /** * Remove query from whitelist */ removeWhitelistedQuery(query: string): void { this.whitelistedQueries.delete(query.toLowerCase().trim()); } /** * Get detection statistics */ getStats(): Record { return { patternsCount: this.patterns.length, whitelistedQueriesCount: this.whitelistedQueries.size, strictMode: this.config?.strictMode ?? false, blockSuspiciousPatterns: this.config?.blockSuspiciousPatterns ?? true }; } /** * Simple hash function for logging purposes */ private hashString(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return hash.toString(16); } /** * Update configuration */ updateConfig(config: Partial): void { this.config = {...this.config, ...config}; // Rebuild patterns if custom patterns changed if (config.customPatterns) { this.patterns = [ ...this.defaultPatterns, ...config.customPatterns ]; } // Rebuild whitelist if changed if (config.whitelistQueries) { this.whitelistedQueries = new Set(config.whitelistQueries); } } }