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
JavaScript
/**
* 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);
}
}