/** * Security utilities for the Bugulma City Resource Graph frontend * Provides additional security measures beyond basic API client security */ /** * Secure random string generation for nonces, tokens, etc. */ export function generateSecureRandom(length = 32): string { const array = new Uint8Array(length); crypto.getRandomValues(array); return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); } /** * Content Security Policy violation handler */ export function setupCSPViolationReporting(): void { if (typeof document !== 'undefined') { document.addEventListener('securitypolicyviolation', (event) => { console.warn('CSP Violation:', { violatedDirective: event.violatedDirective, blockedURI: event.blockedURI, sourceFile: event.sourceFile, lineNumber: event.lineNumber, columnNumber: event.columnNumber, }); // In production, send to monitoring service if (import.meta.env.PROD) { // TODO: Send to monitoring service like Sentry, LogRocket, etc. } }); } } /** * Secure localStorage wrapper with encryption (basic implementation) * Note: This is still vulnerable to XSS, but provides additional protection */ export class SecureStorage { private static readonly PREFIX = 'bugulma_secure_'; static setItem(key: string, value: string): void { try { const prefixedKey = this.PREFIX + key; // Basic obfuscation (not true encryption - for demo purposes) const obfuscated = btoa(encodeURIComponent(value)); localStorage.setItem(prefixedKey, obfuscated); } catch (error) { console.warn('SecureStorage setItem failed:', error); } } static getItem(key: string): string | null { try { const prefixedKey = this.PREFIX + key; const obfuscated = localStorage.getItem(prefixedKey); if (obfuscated) { return decodeURIComponent(atob(obfuscated)); } return null; } catch (error) { console.warn('SecureStorage getItem failed:', error); return null; } } static removeItem(key: string): void { try { const prefixedKey = this.PREFIX + key; localStorage.removeItem(prefixedKey); } catch (error) { console.warn('SecureStorage removeItem failed:', error); } } static clear(): void { try { const keys = Object.keys(localStorage); keys.forEach(key => { if (key.startsWith(this.PREFIX)) { localStorage.removeItem(key); } }); } catch (error) { console.warn('SecureStorage clear failed:', error); } } } /** * Input sanitization for different contexts */ export class InputSanitizer { /** * Sanitize HTML content (removes dangerous tags) */ static sanitizeHtml(input: string): string { // Create a temporary DOM element to leverage browser's HTML parsing const temp = document.createElement('div'); temp.textContent = input; return temp.innerHTML; } /** * Sanitize filename (remove dangerous characters) */ static sanitizeFilename(filename: string): string { return filename.replace(/[^a-zA-Z0-9._-]/g, '_'); } /** * Sanitize URL (ensure it's safe) */ static sanitizeUrl(url: string): string { try { const parsed = new URL(url); // Only allow http/https protocols if (!['http:', 'https:'].includes(parsed.protocol)) { return ''; } return parsed.toString(); } catch { return ''; } } /** * Sanitize SQL-like inputs (remove dangerous characters) */ static sanitizeSqlInput(input: string): string { // Remove or escape common SQL injection characters return input.replace(/['";\\]/g, ''); } } /** * Security monitoring and logging */ export class SecurityMonitor { private static violations: SecurityViolation[] = []; static logViolation(type: string, details: Record): void { const violation: SecurityViolation = { type, details, timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href, }; this.violations.push(violation); console.warn('Security violation logged:', violation); // Keep only last 100 violations to prevent memory issues if (this.violations.length > 100) { this.violations = this.violations.slice(-100); } // In production, send to monitoring service if (import.meta.env.PROD) { this.reportToMonitoring(violation); } } static getViolations(): SecurityViolation[] { return [...this.violations]; } private static reportToMonitoring(violation: SecurityViolation): void { // TODO: Integrate with monitoring service // Example: Sentry, LogRocket, DataDog, etc. try { // Placeholder for monitoring integration console.log('Reporting violation to monitoring service:', violation); } catch (error) { console.error('Failed to report violation:', error); } } } interface SecurityViolation { type: string; details: Record; timestamp: string; userAgent: string; url: string; } /** * Initialize security measures on app startup */ export function initializeSecurity(): void { // Setup CSP violation reporting setupCSPViolationReporting(); // Log security initialization console.log('🔒 Security measures initialized'); // Additional security checks if (typeof window !== 'undefined') { // Check for insecure protocols in production if (import.meta.env.PROD && window.location.protocol === 'http:') { console.warn('🚨 SECURITY WARNING: Application is running over HTTP in production!'); } // Check for missing security headers (basic check) const cspMeta = document.querySelector('meta[http-equiv="Content-Security-Policy"]'); if (!cspMeta) { console.warn('🚨 SECURITY WARNING: Content Security Policy not found!'); } else { // Log CSP configuration for debugging console.log('🔒 CSP configured:', cspMeta.getAttribute('content')); } } } /** * Validate file upload security */ export function validateFileUpload(file: File): { isValid: boolean; error?: string } { // Check file size (10MB limit) const maxSize = 10 * 1024 * 1024; // 10MB if (file.size > maxSize) { return { isValid: false, error: 'File size exceeds 10MB limit' }; } // Check file type const allowedTypes = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', 'text/plain', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', ]; if (!allowedTypes.includes(file.type)) { return { isValid: false, error: 'File type not allowed' }; } // Check filename const sanitizedName = InputSanitizer.sanitizeFilename(file.name); if (sanitizedName !== file.name) { return { isValid: false, error: 'Filename contains invalid characters' }; } return { isValid: true }; }