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
This commit is contained in:
181
admin-ui/js/core/sanitizer.js
Normal file
181
admin-ui/js/core/sanitizer.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user