参考手册阅读约 6 分钟

HTML 实体编码 vs URL 编码:关键区别

HTML 实体编码将 <、>、& 和 " 等字符转换为 HTML 实体(&lt;、&gt;、&amp;、&quot;),以便在网页中安全显示。URL 编码则将字符转换为 %XX 格式,以便在 URL 中安全传输。它们防范的攻击类型不同,适用的场景也各不相同。

什么是 HTML 实体编码?

HTML 实体编码(也称为 HTML 转义)会把在 HTML 中具有特殊含义的字符转换为实体引用,从而让浏览器把它们当作字面字符显示出来,而不是解析为标记。最需要编码的字符是 <>&"'

如果不进行 HTML 编码,像 <script>alert('XSS')</script> 这样的字符串会被浏览器当作 JavaScript 执行。而经过编码后,它会变成 &lt;script&gt;alert(&apos;XSS&apos;)&lt;/script&gt;,并作为无害的文本显示出来。

HTML 实体可以写成两种形式:命名实体,如 &amp;&lt;&quot;;或数字实体,如 &#38;&#60;&#34;。数字实体使用字符的 Unicode 码点,可以表示任意 Unicode 字符。

<!-- HTML 编码之前 -->
<p>The formula is: x < y & y > z</p>
<!-- 浏览器会把 < 解析为一个标签的开始! -->

<!-- HTML 编码之后 -->
<p>The formula is: x &lt; y &amp; y &gt; z</p>
<!-- 浏览器显示为:x < y & y > z -->

<!-- 常见的 HTML 实体 -->
&amp;   → &   (和号 ampersand)
&lt;    → <   (小于号 less than)
&gt;    → >   (大于号 greater than)
&quot;  → "   (双引号 double quote)
&#39;   → '   (单引号 / 撇号 single quote / apostrophe)
&nbsp;  →     (不换行空格 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(十六进制字节值)
空格变为 &nbsp;(不换行空格)或保持原样 %20+
& 变为 &amp; %26
< 变为 &lt; %3C
防范 XSS 攻击、损坏的 HTML 损坏的 URL、注入攻击
适用场景 HTML 文档正文和属性 URL 路径、查询字符串、片段
JS 函数 无内置函数(需使用库或 DOM API) encodeURIComponent()

什么时候两者都需要?

有一些常见情况需要同时用到 HTML 编码和 URL 编码。最常见的场景是把 URL 放入 HTML 属性中,例如 hrefsrc 属性。此时必须先对 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 &amp; Jerry
</a>

<!-- 在这种情况下,% 序列不需要进行 HTML 编码, -->
<!-- 因为 % 不是特殊的 HTML 字符 -->

<!-- 但如果 URL 中包含作为分隔符的 &,则必须对其进行 HTML 编码: -->
<a href="https://example.com/search?q=cats&amp;sort=name">
  Search cats sorted by name
</a>
<!-- 如果没有 &amp;,浏览器可能会把 &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;一定要应用输出编码。

相关文章

试用我们的免费工具