UNPKG

shield-firewall

Version:

AI-Powered Client-Side Web Application Firewall - Zero-Knowledge, Predictive Security Protection

603 lines (518 loc) 16.9 kB
/** * Network Request Interceptor for SHIELD.js * Intercepts and analyzes all network requests (fetch and XMLHttpRequest) * Provides comprehensive request/response monitoring and blocking */ import logger from '../utils/logger.js'; class NetworkInterceptor { constructor(threatDetector) { this.threatDetector = threatDetector; this.originalFetch = window.fetch; this.originalXMLHttpRequest = window.XMLHttpRequest; this.isActive = false; this.requestQueue = new Map(); this.requestId = 0; // Rate limiting this.requestCounts = new Map(); this.rateLimitWindow = 60000; // 1 minute this.rateLimitMax = 100; // Max requests per window // Request caching this.responseCache = new Map(); this.cacheMaxAge = 300000; // 5 minutes } /** * Initialize network interception */ initialize() { this.interceptFetch(); this.interceptXMLHttpRequest(); this.isActive = true; logger.info('Network Interceptor initialized'); } /** * Intercept fetch requests */ interceptFetch() { window.fetch = async (input, init = {}) => { const requestId = ++this.requestId; const request = new Request(input, init); // Pre-request analysis const preAnalysis = await this.analyzeOutgoingRequest(request, requestId); if (preAnalysis.action === 'block') { logger.threatDetected('request_blocked', { type: 'fetch', url: request.url, method: request.method, reasons: preAnalysis.reasons, score: preAnalysis.score }); // Return blocked response return new Response( JSON.stringify({ error: 'Request blocked by SHIELD', code: 'BLOCKED' }), { status: 403, statusText: 'Forbidden', headers: { 'Content-Type': 'application/json' } } ); } // Check rate limiting if (this.isRateLimited(request.url)) { logger.threatDetected('rate_limit_exceeded', { url: request.url, method: request.method }); return new Response( JSON.stringify({ error: 'Rate limit exceeded', code: 'RATE_LIMITED' }), { status: 429, statusText: 'Too Many Requests', headers: { 'Content-Type': 'application/json' } } ); } // Check cache const cacheKey = await this.generateCacheKey(request); const cachedResponse = this.getCachedResponse(cacheKey); if (cachedResponse && this.isCacheValid(cachedResponse)) { logger.debug('Serving cached response', { url: request.url }); return cachedResponse.response.clone(); } // Store request info this.requestQueue.set(requestId, { request, startTime: Date.now(), cacheKey }); try { // Make the actual request const response = await this.originalFetch(request); // Post-request analysis const postAnalysis = await this.analyzeIncomingResponse(response, request.url, requestId); if (postAnalysis.action === 'block') { logger.threatDetected('response_blocked', { type: 'fetch', url: request.url, status: response.status, reasons: postAnalysis.reasons, score: postAnalysis.score }); return new Response( JSON.stringify({ error: 'Response blocked by SHIELD', code: 'BLOCKED' }), { status: 403, statusText: 'Forbidden', headers: { 'Content-Type': 'application/json' } } ); } // Clone response for caching and analysis const responseClone = response.clone(); // Cache successful responses if (response.ok && this.shouldCache(request)) { this.cacheResponse(cacheKey, responseClone); } // Log successful request this.logRequest(requestId, request, response, 'success'); return response; } catch (error) { // Log failed request this.logRequest(requestId, request, null, 'error', error); throw error; } finally { // Cleanup this.requestQueue.delete(requestId); } }; } /** * Intercept XMLHttpRequest */ interceptXMLHttpRequest() { const self = this; const OriginalXHR = this.originalXMLHttpRequest; window.XMLHttpRequest = function() { const xhr = new OriginalXHR(); const requestId = ++self.requestId; // Store original methods const originalOpen = xhr.open; const originalSend = xhr.send; const originalSetRequestHeader = xhr.setRequestHeader; const requestData = { method: null, url: null, headers: {}, body: null }; // Override open xhr.open = function(method, url, async = true, user = null, password = null) { requestData.method = method; requestData.url = url; // Check for suspicious URLs if (self.isSuspiciousUrl(url)) { logger.threatDetected('suspicious_xhr_url', { url, method }); } return originalOpen.call(this, method, url, async, user, password); }; // Override setRequestHeader xhr.setRequestHeader = function(header, value) { requestData.headers[header.toLowerCase()] = value; return originalSetRequestHeader.call(this, header, value); }; // Override send xhr.send = function(body) { requestData.body = body; // Create a Request-like object for analysis const fakeRequest = { url: requestData.url, method: requestData.method, headers: new Headers(requestData.headers), body: body }; // Analyze request self.analyzeOutgoingRequest(fakeRequest, requestId).then(analysis => { if (analysis.action === 'block') { logger.threatDetected('xhr_request_blocked', { url: requestData.url, method: requestData.method, reasons: analysis.reasons, score: analysis.score }); // Abort the request xhr.abort(); return; } // Check rate limiting if (self.isRateLimited(requestData.url)) { logger.threatDetected('xhr_rate_limit_exceeded', { url: requestData.url, method: requestData.method }); xhr.abort(); return; } // Proceed with request self.requestQueue.set(requestId, { request: fakeRequest, startTime: Date.now(), xhr: true }); // Setup response monitoring self.setupXHRResponseMonitoring(xhr, requestId); originalSend.call(this, body); }).catch(error => { logger.error('XHR request analysis failed', { error: error.message }); originalSend.call(this, body); }); }; return xhr; }; // Copy prototype window.XMLHttpRequest.prototype = OriginalXHR.prototype; } /** * Setup XHR response monitoring * @param {XMLHttpRequest} xhr - XHR object * @param {number} requestId - Request ID */ setupXHRResponseMonitoring(xhr, requestId) { const originalOnLoad = xhr.onload; const originalOnError = xhr.onerror; xhr.onload = function(event) { const requestInfo = this.interceptor.requestQueue.get(requestId); if (requestInfo) { // Create fake response object const fakeResponse = { status: xhr.status, statusText: xhr.statusText, headers: this.parseXHRHeaders(xhr), url: xhr.responseURL, body: xhr.responseText }; // Analyze response this.interceptor.analyzeIncomingResponse(fakeResponse, requestInfo.request.url, requestId) .then(analysis => { if (analysis.action === 'block') { logger.threatDetected('xhr_response_blocked', { url: requestInfo.request.url, status: xhr.status, reasons: analysis.reasons, score: analysis.score }); // Clear response xhr.responseText = ''; xhr.responseXML = null; xhr.status = 403; xhr.statusText = 'Forbidden'; } else { // Log successful request this.interceptor.logRequest(requestId, requestInfo.request, fakeResponse, 'success'); } }); } // Call original onload if (originalOnLoad) { originalOnLoad.call(this, event); } }.bind({ interceptor: this }); xhr.onerror = function(event) { const requestInfo = this.interceptor.requestQueue.get(requestId); if (requestInfo) { this.interceptor.logRequest(requestId, requestInfo.request, null, 'error'); } // Call original onerror if (originalOnError) { originalOnError.call(this, event); } }.bind({ interceptor: this }); } /** * Parse XHR response headers * @param {XMLHttpRequest} xhr - XHR object * @returns {Object} Parsed headers */ parseXHRHeaders(xhr) { const headers = {}; const headerString = xhr.getAllResponseHeaders(); if (headerString) { const headerLines = headerString.split('\n'); for (const line of headerLines) { const colonIndex = line.indexOf(':'); if (colonIndex > 0) { const name = line.substring(0, colonIndex).trim().toLowerCase(); const value = line.substring(colonIndex + 1).trim(); headers[name] = value; } } } return headers; } /** * Analyze outgoing request * @param {Request} request - Request object * @param {number} requestId - Request ID * @returns {Promise<Object>} Analysis result */ async analyzeOutgoingRequest(request, requestId) { try { return await this.threatDetector.analyzeRequest(request); } catch (error) { logger.error('Request analysis failed', { error: error.message, requestId }); return { action: 'allow', score: 0, reasons: [] }; } } /** * Analyze incoming response * @param {Response|Object} response - Response object * @param {string} requestUrl - Original request URL * @param {number} requestId - Request ID * @returns {Promise<Object>} Analysis result */ async analyzeIncomingResponse(response, requestUrl, requestId) { try { return await this.threatDetector.analyzeResponse(response, requestUrl); } catch (error) { logger.error('Response analysis failed', { error: error.message, requestId }); return { action: 'allow', score: 0, reasons: [] }; } } /** * Check if URL is suspicious * @param {string} url - URL to check * @returns {boolean} Is suspicious */ isSuspiciousUrl(url) { try { const urlObj = new URL(url); // Check for suspicious schemes const suspiciousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:']; if (suspiciousSchemes.includes(urlObj.protocol)) { return true; } // Check for very long URLs if (url.length > 2048) { return true; } // Check for suspicious characters const suspiciousChars = /[<>'"&\\]/; if (suspiciousChars.test(url)) { return true; } return false; } catch { return true; // Invalid URLs are suspicious } } /** * Check rate limiting * @param {string} url - Request URL * @returns {boolean} Is rate limited */ isRateLimited(url) { try { const domain = new URL(url).hostname; const now = Date.now(); const windowStart = now - this.rateLimitWindow; if (!this.requestCounts.has(domain)) { this.requestCounts.set(domain, []); } const requests = this.requestCounts.get(domain); // Remove old requests const recentRequests = requests.filter(time => time > windowStart); recentRequests.push(now); this.requestCounts.set(domain, recentRequests); return recentRequests.length > this.rateLimitMax; } catch { return false; } } /** * Generate cache key for request * @param {Request} request - Request object * @returns {Promise<string>} Cache key */ async generateCacheKey(request) { const keyData = { url: request.url, method: request.method, headers: {} }; // Include important headers const cacheHeaders = ['accept', 'accept-language', 'user-agent']; for (const header of cacheHeaders) { const value = request.headers.get(header); if (value) { keyData.headers[header] = value; } } const keyString = JSON.stringify(keyData); return await crypto.subtle.digest('SHA-256', new TextEncoder().encode(keyString)) .then(hash => Array.from(new Uint8Array(hash)) .map(b => b.toString(16).padStart(2, '0')) .join('')); } /** * Check if request should be cached * @param {Request} request - Request object * @returns {boolean} Should cache */ shouldCache(request) { // Only cache GET requests if (request.method !== 'GET') return false; // Don't cache sensitive URLs const sensitivePatterns = [/\/api\//, /\/auth\//, /\/user\//, /\?/, /#/]; if (sensitivePatterns.some(pattern => pattern.test(request.url))) { return false; } return true; } /** * Cache response * @param {string} key - Cache key * @param {Response} response - Response to cache */ cacheResponse(key, response) { this.responseCache.set(key, { response: response.clone(), timestamp: Date.now() }); // Limit cache size if (this.responseCache.size > 100) { const oldestKey = this.responseCache.keys().next().value; this.responseCache.delete(oldestKey); } } /** * Get cached response * @param {string} key - Cache key * @returns {Object|null} Cached response */ getCachedResponse(key) { return this.responseCache.get(key) || null; } /** * Check if cached response is still valid * @param {Object} cached - Cached response data * @returns {boolean} Is valid */ isCacheValid(cached) { return (Date.now() - cached.timestamp) < this.cacheMaxAge; } /** * Log completed request * @param {number} requestId - Request ID * @param {Request} request - Request object * @param {Response} response - Response object * @param {string} status - Request status * @param {Error} error - Error object */ logRequest(requestId, request, response, status, error = null) { const duration = Date.now() - this.requestQueue.get(requestId)?.startTime; const logData = { requestId, url: request.url, method: request.method, status, duration }; if (response) { logData.responseStatus = response.status; logData.contentType = response.headers?.get('content-type'); } if (error) { logData.error = error.message; } logger.performance('network_request', duration, 'ms'); logger.debug('Network request completed', logData); } /** * Get network statistics * @returns {Object} Statistics */ getStats() { const domains = Array.from(this.requestCounts.keys()); const totalRequests = domains.reduce((sum, domain) => sum + this.requestCounts.get(domain).length, 0); return { active: this.isActive, queuedRequests: this.requestQueue.size, cachedResponses: this.responseCache.size, monitoredDomains: domains.length, totalRequests, rateLimitWindow: this.rateLimitWindow, rateLimitMax: this.rateLimitMax }; } /** * Clear caches and counters */ clearCaches() { this.requestCounts.clear(); this.responseCache.clear(); this.requestQueue.clear(); logger.info('Network interceptor caches cleared'); } /** * Set rate limiting parameters * @param {number} maxRequests - Max requests per window * @param {number} windowMs - Window size in milliseconds */ setRateLimit(maxRequests, windowMs) { this.rateLimitMax = maxRequests; this.rateLimitWindow = windowMs; logger.info('Rate limiting updated', { maxRequests, windowMs }); } /** * Restore original network functions */ destroy() { window.fetch = this.originalFetch; window.XMLHttpRequest = this.originalXMLHttpRequest; this.clearCaches(); this.isActive = false; } } export default NetworkInterceptor;