/* =============================================== HTML SANITIZER - XSS Protection Prevents cross-site scripting attacks =============================================== */ class Sanitizer { /** * Escape HTML special characters * @param {string} str - String to escape * @returns {string} Escaped string */ static escapeHTML(str) { if (typeof str !== 'string') return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } /** * Sanitize HTML content (basic implementation) * For production, consider using DOMPurify library * @param {string} html - HTML to sanitize * @returns {string} Sanitized HTML */ static sanitizeHTML(html) { if (typeof html !== 'string') return ''; // Create temporary element const temp = document.createElement('div'); temp.innerHTML = html; // Remove dangerous elements const dangerousTags = ['script', 'iframe', 'object', 'embed', 'link', 'style']; dangerousTags.forEach(tag => { const elements = temp.querySelectorAll(tag); elements.forEach(el => el.remove()); }); // Remove dangerous attributes const dangerousAttrs = ['onclick', 'onload', 'onerror', 'onmouseover', 'onfocus', 'onblur']; const allElements = temp.querySelectorAll('*'); allElements.forEach(el => { dangerousAttrs.forEach(attr => { if (el.hasAttribute(attr)) { el.removeAttribute(attr); } }); // Remove javascript: URLs ['href', 'src', 'action'].forEach(attr => { if (el.hasAttribute(attr)) { const value = el.getAttribute(attr); if (value && value.toLowerCase().startsWith('javascript:')) { el.removeAttribute(attr); } } }); }); return temp.innerHTML; } /** * Validate and sanitize URL * @param {string} url - URL to validate * @returns {string|null} Sanitized URL or null if invalid */ static sanitizeURL(url) { if (typeof url !== 'string') return null; try { const parsed = new URL(url, window.location.origin); // Only allow http, https, mailto const allowedProtocols = ['http:', 'https:', 'mailto:']; if (!allowedProtocols.includes(parsed.protocol)) { return null; } return parsed.href; } catch (e) { // Invalid URL return null; } } /** * Create safe HTML element with text content * @param {string} tag - HTML tag name * @param {string} text - Text content * @param {Object} attributes - Safe attributes * @returns {HTMLElement} Created element */ static createElement(tag, text = '', attributes = {}) { const element = document.createElement(tag); if (text) { element.textContent = text; } // Set safe attributes const safeAttrs = ['class', 'id', 'data-', 'aria-', 'role', 'title', 'alt']; Object.entries(attributes).forEach(([key, value]) => { const isSafe = safeAttrs.some(safe => key.startsWith(safe)); if (isSafe) { element.setAttribute(key, value); } }); return element; } /** * Sanitize object for safe JSON stringification * @param {any} obj - Object to sanitize * @returns {any} Sanitized object */ static sanitizeObject(obj) { if (obj === null || obj === undefined) return obj; if (typeof obj === 'string') { return this.escapeHTML(obj); } if (Array.isArray(obj)) { return obj.map(item => this.sanitizeObject(item)); } if (typeof obj === 'object') { const sanitized = {}; Object.entries(obj).forEach(([key, value]) => { sanitized[key] = this.sanitizeObject(value); }); return sanitized; } return obj; } /** * Strip all HTML tags * @param {string} html - HTML string * @returns {string} Plain text */ static stripHTML(html) { if (typeof html !== 'string') return ''; const temp = document.createElement('div'); temp.innerHTML = html; return temp.textContent || temp.innerText || ''; } /** * Truncate text safely * @param {string} text - Text to truncate * @param {number} maxLength - Maximum length * @param {string} suffix - Suffix to add (default: '...') * @returns {string} Truncated text */ static truncate(text, maxLength, suffix = '...') { if (typeof text !== 'string') return ''; if (text.length <= maxLength) return text; return text.substring(0, maxLength - suffix.length) + suffix; } } // Make Sanitizer globally available window.Sanitizer = Sanitizer;