Files
dss/admin-ui/js/core/sanitizer.js
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
2025-12-09 18:45:48 -03:00

182 lines
4.3 KiB
JavaScript

/**
* HTML Sanitization Module
*
* Provides secure HTML rendering with DOMPurify integration.
* Ensures consistent XSS protection across the application.
*/
/**
* Sanitize HTML content for safe rendering
*
* @param {string} html - HTML to sanitize
* @param {object} options - DOMPurify options
* @returns {string} Sanitized HTML
*/
export function sanitizeHtml(html, options = {}) {
const defaultOptions = {
ALLOWED_TAGS: [
'div', 'span', 'p', 'a', 'button', 'input', 'textarea',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'table', 'tr', 'td', 'th', 'thead', 'tbody',
'strong', 'em', 'code', 'pre', 'blockquote',
'svg', 'img'
],
ALLOWED_ATTR: [
'class', 'id', 'style', 'data-*',
'href', 'target', 'rel',
'type', 'placeholder', 'disabled', 'checked', 'name', 'value',
'alt', 'src', 'width', 'height',
'aria-label', 'aria-describedby', 'aria-invalid', 'role'
],
KEEP_CONTENT: true,
RETURN_DOM: false
};
const mergedOptions = { ...defaultOptions, ...options };
// Use DOMPurify if available (loaded in HTML)
if (typeof DOMPurify !== 'undefined') {
return DOMPurify.sanitize(html, mergedOptions);
}
// Fallback: escape HTML (basic protection)
console.warn('DOMPurify not available, using basic HTML escaping');
return escapeHtml(html);
}
/**
* Escape HTML special characters (basic XSS protection)
*
* @param {string} text - Text to escape
* @returns {string} Escaped text
*/
export function escapeHtml(text) {
const map = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, char => map[char]);
}
/**
* Safely set innerHTML on an element
*
* @param {HTMLElement} element - Target element
* @param {string} html - HTML to set (will be sanitized)
* @param {object} options - Sanitization options
*/
export function setSafeHtml(element, html, options = {}) {
if (!element) {
console.warn('setSafeHtml: element is null or undefined');
return;
}
element.innerHTML = sanitizeHtml(html, options);
}
/**
* Sanitize text for safe display (no HTML)
*
* @param {string} text - Text to sanitize
* @returns {string} Sanitized text
*/
export function sanitizeText(text) {
return escapeHtml(String(text || ''));
}
/**
* Sanitize URL for safe linking
*
* @param {string} url - URL to sanitize
* @returns {string} Safe URL or empty string
*/
export function sanitizeUrl(url) {
try {
// Only allow safe protocols
const allowedProtocols = ['http:', 'https:', 'mailto:', 'tel:', 'data:'];
const urlObj = new URL(url, window.location.href);
if (allowedProtocols.includes(urlObj.protocol)) {
return url;
}
console.warn(`Unsafe URL protocol: ${urlObj.protocol}`);
return '';
} catch (e) {
console.warn(`Invalid URL: ${url}`);
return '';
}
}
/**
* Create safe HTML element from template literal
*
* Usage:
* const html = createSafeHtml`
* <div class="card">
* <h3>${title}</h3>
* <p>${description}</p>
* </div>
* `;
*
* @param {string[]} strings - Template strings
* @param {...any} values - Interpolated values (will be escaped)
* @returns {string} Safe HTML
*/
export function createSafeHtml(strings, ...values) {
let result = '';
for (let i = 0; i < strings.length; i++) {
result += strings[i];
if (i < values.length) {
// Escape all interpolated values
const value = values[i];
if (value === null || value === undefined) {
result += '';
} else if (typeof value === 'object') {
result += sanitizeText(JSON.stringify(value));
} else {
result += sanitizeText(String(value));
}
}
}
return result;
}
/**
* Validate HTML structure without executing scripts
*
* @param {string} html - HTML to validate
* @returns {boolean} True if HTML is well-formed
*/
export function validateHtml(html) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Check for parsing errors
if (doc.getElementsByTagName('parsererror').length > 0) {
return false;
}
return true;
} catch (e) {
return false;
}
}
export default {
sanitizeHtml,
escapeHtml,
setSafeHtml,
sanitizeText,
sanitizeUrl,
createSafeHtml,
validateHtml
};