UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

214 lines (185 loc) 4.97 kB
/** * StyleSanitizer - Secure CSS and HTML sanitization for custom styling * Provides comprehensive security validation for user-generated styling content */ import DOMPurify from 'dompurify'; export class StyleSanitizer { constructor() { this.allowedCSSProperties = [ 'color', 'background-color', 'background', 'font-family', 'font-size', 'font-weight', 'line-height', 'text-align', 'margin', 'padding', 'border', 'border-radius', 'box-shadow', 'opacity', 'transition', 'transform', 'animation', 'display', 'flex', 'gap', 'width', 'max-width', 'min-width', 'height', 'max-height', 'min-height', ]; this.blockedCSSPatterns = [ /expression\(/i, /javascript:/i, /vbscript:/i, /onload/i, /onerror/i, /onclick/i, /@import/i, /position\s*:\s*fixed/i, // Prevent overlay attacks /z-index\s*:\s*99999/i, ]; this.configureDOMPurify(); } configureDOMPurify() { // Configure for widget content - will be enhanced when DOMPurify is available this.purifyConfig = { ALLOWED_TAGS: [ 'div', 'span', 'p', 'br', 'strong', 'em', 'b', 'i', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'code', 'pre', ], ALLOWED_ATTR: ['class', 'id', 'href', 'src', 'alt', 'title'], FORBID_TAGS: ['style', 'script', 'iframe', 'form', 'input'], FORBID_ATTR: ['style', 'onclick', 'onerror', 'onload'], }; } sanitizeCSS(cssText) { if (!cssText || typeof cssText !== 'string') { return ''; } // Remove comments cssText = cssText.replace(/\/\*[\s\S]*?\*\//g, ''); // Check for blocked patterns for (const pattern of this.blockedCSSPatterns) { if (pattern.test(cssText)) { console.warn('Blocked CSS pattern detected:', pattern); return ''; } } // Parse and validate CSS rules const sanitizedRules = []; const ruleRegex = /([^{]+)\{([^}]+)\}/g; let match; while ((match = ruleRegex.exec(cssText)) !== null) { const selector = match[1].trim(); const properties = match[2].trim(); // Validate selector (must start with .besper- for security) if (!selector.includes('.besper-')) { continue; } // Validate properties const sanitizedProperties = this.sanitizeProperties(properties); if (sanitizedProperties) { sanitizedRules.push(`${selector} { ${sanitizedProperties} }`); } } return sanitizedRules.join('\n'); } sanitizeProperties(properties) { const props = properties.split(';').filter(p => p.trim()); const sanitized = []; for (const prop of props) { const colonIndex = prop.indexOf(':'); if (colonIndex === -1) continue; const property = prop.slice(0, colonIndex).trim(); const value = prop.slice(colonIndex + 1).trim(); if (this.allowedCSSProperties.includes(property)) { // Additional validation for specific properties if (this.isValidPropertyValue(property, value)) { sanitized.push(`${property}: ${value}`); } } } return sanitized.join('; '); } isValidPropertyValue(property, value) { // Color validation if (property.includes('color')) { return this.isValidColor(value); } // Size validation if ( ['font-size', 'width', 'height', 'padding', 'margin'].includes(property) ) { return this.isValidSize(value); } // Font validation if (property === 'font-family') { return this.isValidFontFamily(value); } return true; } isValidColor(color) { const validPatterns = [ /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, /^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/, /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$/, ]; const validColorNames = [ 'white', 'black', 'red', 'green', 'blue', 'yellow', 'transparent', 'currentColor', ]; return ( validPatterns.some(pattern => pattern.test(color)) || validColorNames.includes(color.toLowerCase()) ); } isValidSize(size) { return ( /^\d+(\.\d+)?(px|em|rem|%|vh|vw)$/.test(size) || ['auto', 'inherit', 'initial'].includes(size) ); } isValidFontFamily(font) { // Remove quotes and check for common injection attempts const cleaned = font.replace(/['"]/g, ''); return !/[<>{}();]/.test(cleaned); } sanitizeHTML(html) { if (!html || typeof html !== 'string') { return ''; } // Use DOMPurify for comprehensive HTML sanitization return DOMPurify.sanitize(html, this.purifyConfig); } }