refactor: 模块化架构重构
将单文件巨型 IIFE (1608行) 拆分为 14 个 ES Module: 核心层: - js/state.js - 轻量级响应式状态管理 (发布-订阅) - js/utils.js - 通用工具函数 - js/sanitizer.js - HTML XSS 净化器 - js/marked-config.js - Markdown 渲染器配置 组件层: - js/components/chat-area.js - 消息渲染/流式更新/导出 - js/components/input-area.js - 文本输入/图片上传/发送 - js/components/history-modal.js - 历史记录面板/搜索/分页 - js/components/settings-modal.js - 设置面板/数据管理 - js/components/header.js - 顶部导航/连接状态 - js/components/model-bar.js - 模型选择栏 - js/components/toast.js - Toast 通知 - js/components/lightbox.js - 图片预览 入口层: - js/app.js - 主入口/初始化协调 index.html: 1608行 → 204行纯模板
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Sanitizer - HTML 净化器
|
||||
* 基于白名单策略,防御 XSS 攻击
|
||||
*/
|
||||
|
||||
export const SAFE_URI_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:', 'ftp:', 'ftps:', 'xmpp:']);
|
||||
export const SAFE_DATA_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml']);
|
||||
|
||||
const ALLOWED_TAGS = new Set([
|
||||
'a', 'abbr', 'b', 'blockquote', 'br', 'code', 'del', 'details', 'div',
|
||||
'dl', 'dt', 'dd', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr',
|
||||
'i', 'img', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'rp', 'rt',
|
||||
'ruby', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub',
|
||||
'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead',
|
||||
'tr', 'u', 'ul', 'var', 'input',
|
||||
]);
|
||||
|
||||
const ALLOWED_ATTRS = new Set([
|
||||
'href', 'src', 'alt', 'title', 'width', 'height', 'align',
|
||||
'target', 'rel', 'class', 'id', 'type', 'checked', 'disabled',
|
||||
'start', 'value', 'scope', 'rowspan', 'colspan', 'lang',
|
||||
]);
|
||||
|
||||
const DANGEROUS_PREFIXES = ['on', 'xlink:href', 'xmlns:'];
|
||||
|
||||
/**
|
||||
* 净化 HTML 字符串,移除危险标签和属性
|
||||
* @param {string} html - 原始 HTML
|
||||
* @returns {string} 净化后的 HTML
|
||||
*/
|
||||
export function sanitize(html) {
|
||||
try {
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = html;
|
||||
const root = template.content;
|
||||
|
||||
// 删除注释节点
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
||||
const comments = [];
|
||||
while (walker.nextNode()) comments.push(walker.currentNode);
|
||||
comments.forEach(c => c.remove());
|
||||
|
||||
// 遍历所有元素(先收集再处理,避免动态集合问题)
|
||||
const elements = [...root.querySelectorAll('*')];
|
||||
for (const el of elements) {
|
||||
if (!el.parentNode) continue;
|
||||
|
||||
const tag = el.tagName.toLowerCase();
|
||||
|
||||
if (!ALLOWED_TAGS.has(tag)) {
|
||||
while (el.firstChild) el.parentNode.insertBefore(el.firstChild, el);
|
||||
el.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 清理属性
|
||||
for (const attr of [...el.attributes]) {
|
||||
const name = attr.name.toLowerCase();
|
||||
|
||||
if (DANGEROUS_PREFIXES.some(p => name.startsWith(p))) {
|
||||
el.removeAttribute(attr.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ALLOWED_ATTRS.has(name)) {
|
||||
el.removeAttribute(attr.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// URI 协议检查
|
||||
if ((name === 'href' || name === 'src' || name === 'action') && attr.value) {
|
||||
const val = attr.value.trim().toLowerCase();
|
||||
if (/^(javascript|vbscript|data|file):/i.test(val)) {
|
||||
const mimeMatch = val.match(/^data:([^;,]+)/i);
|
||||
if (!mimeMatch || !SAFE_DATA_TYPES.has(mimeMatch[1].toLowerCase())) {
|
||||
el.removeAttribute(attr.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root.innerHTML;
|
||||
} catch (e) {
|
||||
console.warn('[Sanitizer] 净化失败,返回原始文本:', e);
|
||||
return html;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user