HTML 实体编码 vs URL 编码:关键区别
HTML 实体编码将 <、>、& 和 " 等字符转换为 HTML 实体(<、>、&、"),以便在网页中安全显示。URL 编码则将字符转换为 %XX 格式,以便在 URL 中安全传输。它们防范的攻击类型不同,适用的场景也各不相同。
什么是 HTML 实体编码?
HTML 实体编码(也称为 HTML 转义)会把在 HTML 中具有特殊含义的字符转换为实体引用,从而让浏览器把它们当作字面字符显示出来,而不是解析为标记。最需要编码的字符是 <、>、&、" 和 '。
如果不进行 HTML 编码,像 <script>alert('XSS')</script> 这样的字符串会被浏览器当作 JavaScript 执行。而经过编码后,它会变成 <script>alert('XSS')</script>,并作为无害的文本显示出来。
HTML 实体可以写成两种形式:命名实体,如 &、< 和 ";或数字实体,如 &、< 和 "。数字实体使用字符的 Unicode 码点,可以表示任意 Unicode 字符。
<!-- HTML 编码之前 -->
<p>The formula is: x < y & y > z</p>
<!-- 浏览器会把 < 解析为一个标签的开始! -->
<!-- HTML 编码之后 -->
<p>The formula is: x < y & y > z</p>
<!-- 浏览器显示为:x < y & y > z -->
<!-- 常见的 HTML 实体 -->
& → & (和号 ampersand)
< → < (小于号 less than)
> → > (大于号 greater than)
" → " (双引号 double quote)
' → ' (单引号 / 撇号 single quote / apostrophe)
→ (不换行空格 non-breaking space)
什么是 URL 编码?
URL 编码(percent-encoding,百分号编码)会把在 URL 中不安全的字符转换为一个百分号,后面跟着两位表示字节值的十六进制数字。它由 RFC 3986 定义,确保 URL 只包含可以在互联网上安全传输的有效 ASCII 字符。
URL 编码之所以必要,是因为 URL 有严格的语法,其中某些字符充当分隔符(/、?、&、#、=)。当这些字符出现在数据中(比如查询参数的值里)时,就必须对它们进行编码,以防被解析为结构性元素。
// URL 编码示例
space → %20
& → %26
= → %3D
? → %3F
# → %23
/ → %2F
+ → %2B
// 带有已编码查询参数的完整 URL
https://example.com/search?q=salt%20%26%20pepper
// q 的值为 "salt & pepper"
对比表
| 特性 | HTML 实体编码 | URL 编码 |
|---|---|---|
| 用途 | 在 HTML 页面中安全显示 | 在 URL 中安全传输 |
| 标准 | HTML/WHATWG 规范 | RFC 3986 |
| 格式 | &name; 或 &#number; |
%XX(十六进制字节值) |
| 空格变为 | (不换行空格)或保持原样 |
%20 或 + |
| & 变为 | & |
%26 |
| < 变为 | < |
%3C |
| 防范 | XSS 攻击、损坏的 HTML | 损坏的 URL、注入攻击 |
| 适用场景 | HTML 文档正文和属性 | URL 路径、查询字符串、片段 |
| JS 函数 | 无内置函数(需使用库或 DOM API) | encodeURIComponent() |
什么时候两者都需要?
有一些常见情况需要同时用到 HTML 编码和 URL 编码。最常见的场景是把 URL 放入 HTML 属性中,例如 href 或 src 属性。此时必须先对 URL 进行正确的 URL 编码,然后再对结果中任何特殊的 HTML 字符进行 HTML 编码。
<!-- 第 1 步:对查询参数的值进行 URL 编码 -->
<!-- 值:"Tom & Jerry" → URL 编码后:"Tom%20%26%20Jerry" -->
<!-- 第 2 步:构建 URL -->
<!-- URL:https://example.com/search?q=Tom%20%26%20Jerry -->
<!-- 第 3 步:对 URL 进行 HTML 编码,以便用在 href 属性中 -->
<a href="https://example.com/search?q=Tom%20%26%20Jerry">
Search for Tom & Jerry
</a>
<!-- 在这种情况下,% 序列不需要进行 HTML 编码, -->
<!-- 因为 % 不是特殊的 HTML 字符 -->
<!-- 但如果 URL 中包含作为分隔符的 &,则必须对其进行 HTML 编码: -->
<a href="https://example.com/search?q=cats&sort=name">
Search cats sorted by name
</a>
<!-- 如果没有 &,浏览器可能会把 &sort 解析为一个 HTML 实体 -->
在 React、Vue 和 Angular 等现代框架中,当你使用模板表达式或 JSX 时,HTML 编码会被自动处理。不过,在用动态数据构建 URL 时,URL 编码仍然需要显式进行。
// React —— HTML 编码是自动的,URL 编码不是
function SearchLink({ query }) {
// URL 编码必须显式进行
const url = '/search?q=' + encodeURIComponent(query);
// JSX 中的 HTML 编码是自动的
return <a href={url}>Search for {query}</a>;
// React 会自动对 href 和文本内容进行 HTML 编码
}
XSS 防护:在什么场景使用哪种编码
跨站脚本(XSS)防护要求根据用户输入所处的上下文,应用正确的编码。在特定上下文中使用错误类型的编码,起不到任何保护作用。
- HTML 正文上下文: 使用 HTML 实体编码。这可以防止像
<script>这样被注入的标签被解析为标记。 - HTML 属性上下文: 使用 HTML 实体编码,并始终为属性值加引号。未加引号的属性即使经过 HTML 编码也可能被突破。
- URL 上下文(href、src): 对动态部分使用 URL 编码,如果要放入 HTML 中,再对整个 URL 进行 HTML 编码。要验证 URL 以安全的协议(
https:)开头,以防出现javascript:这类 URL。 - JavaScript 上下文: 使用 JavaScript 字符串转义(JSON.stringify 或专门的库)。在 JavaScript 上下文中,HTML 编码和 URL 编码都不够用。
- CSS 上下文: 使用 CSS 转义。HTML 编码和 URL 编码都无法防范 CSS 注入。
关键原则是:针对输出上下文进行编码,而不是针对输入来源。同一段用户输入,根据它在响应中出现的位置,可能需要不同的编码。切勿仅依赖输入验证来防范 XSS;一定要应用输出编码。