Files
metona-ollama-desktop/js/sanitizer.js
T
thzxx 38edaee0f0 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行纯模板
2026-04-03 11:34:24 +08:00

89 lines
3.2 KiB
JavaScript

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