beforeinput 事件不能拦截粘贴,因其不触发;必须用 paste 事件配合 preventdefault() 手动处理。需清洗 html 中的 on* 属性、危险 href、零宽字符,并用 range api 精准插入。

beforeinput 事件**不能用于拦截粘贴内容**——它对 paste 操作完全不触发。想控制粘贴行为,必须用 paste 事件。
为什么 beforeinput 对粘贴无效
beforeinput 只在浏览器准备插入**由键盘、输入法、语音或 IME 直接生成的字符**前触发,比如按下一个字母键、中文选词上屏。但粘贴(Ctrl+V / 右键 → 粘贴)是独立的 DOM 事件,其底层流程绕过 beforeinput,直接走 paste → 默认插入 → 触发 input。所有主流浏览器(Chrome/Firefox/Safari/Edge)均如此,无例外。
常见误判现象:
• 在 contenteditable 上监听 beforeinput,然后 Ctrl+V,发现 handler 根本没执行
• 以为 event.inputType === 'insertFromPaste' 是 beforeinput 的一部分 —— 实际上它只出现在后续的 input 事件里,且已是“事后”
正确拦截粘贴:必须用 paste 事件 + preventDefault()
可靠路径只有一条:在目标 contenteditable 元素上监听 paste,开头立刻调用 event.preventDefault(),再手动读取、过滤、插入。
- 绑定位置必须是实际编辑容器本身(如
<div contenteditable="true">),不是父 wrapper 或 document <li>不要用 <code>onpaste属性写法,它在 Shadow DOM 或某些框架中无法保证preventDefault()生效 - 检查
event.clipboardData?.items判断是否含图片:item.type.startsWith('image/');旧版 Safari/微信 X5 需 fallback 到event.clipboardData?.files.length > 0或正则匹配data:image/ - 读 HTML 内容用
event.clipboardData.getData('text/html'),别用getData('text/plain')—— 后者在 macOS Notes、企业微信等场景下常丢换行或返回空 -
<img src="x" onerror="alert(1)">:必须遍历所有元素,移除所有on*属性(onclick、onload等) -
<a href="javascript:alert(1)"></a>:强制重写href,仅保留http://、https://、/、#开头的值,并加rel="noopener noreferrer" - 零宽字符与软连字符:
\u200b(零宽空格)、\u00ad(软连字符)会破坏搜索、分词、移动端渲染,应在插入前从文本节点中全局替换为空字符串
清洗 HTML 时容易漏掉的 XSS 风险点
即使你解析了 text/html 并用 DOMParser 创建干净文档,仍可能漏掉三类危险:
插入清洗后的内容:别依赖 execCommand
document.execCommand('insertText', false, ...) 已废弃,且在现代编辑器中易导致光标跳脱、格式丢失。推荐用 Range API 精准插入:
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(cleanedText));
// 或插入 fragment(若需保留 p/strong 等结构)
}
注意:插入必须同步完成,不能放在 fetch 回调或 setTimeout 里 —— 否则光标位置已变,插入点错乱。
真正难的不是拦截,而是清洗后保持语义结构和光标稳定。很多人卡在“删了 style 却把段落压成一行”或“清完 onerror,<br> 全没了”。这需要你明确白名单标签,并对每个节点做细粒度处理,而不是通配正则替换。











