一行innerHTML背后,可能藏着一次脚本注入
一个评论列表最初只展示纯文本,后来为了方便,把渲染方式改成了字符串模板:把用户名、评论内容拼进一段 HTML,再一次性插入页面。代码变短了,但数据也获得了影响页面结构的能力。
如果评论来自用户输入,攻击者就可能提交带有特殊标签或事件属性的内容。页面把这些内容当作 HTML 解析时,恶意代码便有机会在当前站点的环境中执行。这就是常见的跨站脚本攻击,也叫 XSS。
对于本来只需要展示文字的区域,最直接的办法是使用 textContent,让浏览器按文字处理输入:
function renderComment(comment) {
const article = document.createElement('article');
const author = document.createElement('strong');
const content = document.createElement('p');
author.textContent = comment.author;
content.textContent = comment.text;
article.append(author, content);
return article;
}
document.querySelector('#comments').append(renderComment({
author: '访客',
text: '<img src=x onerror="alert(1)">'
}));
示例假设页面存在 comments 容器。这段输入会被显示成普通文字,而不会被当成图片标签执行。它适合评论、名称、日志等纯文本内容,也避免了自己手写一套容易遗漏的 HTML 转义规则。
如果业务确实允许富文本,就需要另一套方案。不能简单删除字符串中的 <script>:HTML 还有事件属性、危险 URL、嵌套结构等多种表达方式,正则表达式很难正确覆盖浏览器的解析规则。应使用项目已有、持续维护的 HTML 清洗工具,并配置明确的标签和属性白名单。
Markdown 也不是天然安全的输入。有些渲染器允许原始 HTML,有些允许危险链接协议。要检查实际渲染配置,在最终输出进入页面之前完成适当的清洗。前端框架通常会默认转义普通文本,但使用原始 HTML 注入能力时,开发者仍然要承担对应的处理责任。
另外,安全处理要匹配具体位置。textContent 解决的是文本节点展示,不能自动保护所有属性。例如把用户提供的地址直接放进链接,仍然应该检查允许的协议:
function createLink(label, address) {
const url = new URL(address, location.origin);
if (!['https:', 'http:'].includes(url.protocol)) {
throw new Error('不支持的链接协议');
}
const link = document.createElement('a');
link.textContent = label;
link.href = url.href;
return link;
}
这段代码允许普通 HTTP 和 HTTPS 地址,拒绝 javascript: 等协议,但它没有限制目标站点。业务如果只允许站内跳转,还应比较来源或使用更严格的路径规则。创建 URL 失败时,也需要由调用方给出合适的错误反馈,避免让一条坏数据中断整个列表。
内容安全策略 CSP 可以进一步限制脚本来源,降低某些注入的影响,但策略必须结合现有资源和部署方式设计。它不能代替正确的输出处理。HttpOnly Cookie 可以限制脚本直接读取会话标识,却也不能保证注入后账号操作绝对安全,因为恶意脚本仍可能在当前页面发起请求。
验证时,除了正常中文和换行,还可以在测试环境输入尖括号、引号、事件属性字符串以及危险链接协议,观察它们是否保持为文本或被明确拒绝。对富文本,应检查允许的格式保留正常,禁止的结构也确实被清理。测试输入不必真的发送到其他用户可见的公共内容中。
审查代码时,可以先搜索 innerHTML、原始 HTML 渲染接口和动态创建脚本的位置,再追踪数据来源。很多问题并不需要重写整个页面,只要在数据进入浏览器解析器的关键位置,选对原生接口并守住边界,就能消除一类反复出现的风险。
- 点赞
- 收藏
- 关注作者
评论(0)