为什么你的 <b> 变成了 %3Cb%3E?一文读懂 URL 编码机制与误区

25 阅读4分钟

摘要:在前端开发与安全测试中,我们常看到 URL 参数被自动转换成 % 开头的乱码。很多人误以为这是某种安全加密或 XSS 防御机制,但事实并非如此。本文将从 RFC 标准出发,解析 URL 编码的底层逻辑、触发时机以及它与 Web 安全的真实关系。

1. 现象还原

在进行 Web 安全测试(如 XSS 注入)时,我们尝试在 URL 参数中输入 HTML 标签:

http://localhost:8080/api?name=<b>test</b>

然而,当我们查看网络请求(Network)或地址栏时,发现它变成了:

http://localhost:8080/api?name=%3Cb%3Etest%3C%2Fb%3E

初学者容易产生两个误区:

  1. 误区一:这是浏览器在帮我拦截 XSS 攻击。
  2. 误区二:后端收到的就是乱码,需要手动写代码解码。

真相是:这只是 HTTP 协议为了“交通合规”做的标准动作,与安全防御毫无关系。

2. 为什么要编码?(RFC 3986 标准)

URL(统一资源定位符)的设计初衷是只能通过 ASCII 字符集在互联网上传输。为了避免歧义和保证传输安全,IETF 在 RFC 3986 标准中将字符分为了三类:

2.1 绝对安全的字符(Unreserved)

不需要编码,走到哪里都是原样。

  • 包含A-Z a-z 0-9 以及 - _ . ~
  • 注意:很多开发者不知道波浪号 ~ 其实是不需要编码的。

2.2 保留字符(Reserved)

在 URL 结构中有特殊语法含义的字符。

  • 包含! * ' ( ) ; : @ & = + $ , / ? % # [ ]

  • 规则:如果这些字符只是作为单纯的参数值(而不是语法分隔符),就必须编码

    • 例子:参数值里包含 &,如果不转义为 %26,服务器会认为这是下一个参数的开始。

2.3 不安全字符(Unsafe)

容易引起解析歧义或不可打印的字符。

  • 包含:空格、<>"{}|、`^`、 ` `` 以及所有非 ASCII 字符(如中文)。

  • 规则必须编码

    • 例子<> 容易被误认为是 HTML 标签的边界,所以必须转义。

3. 编码原理:百分号编码 (Percent-Encoding)

URL 编码的核心算法非常简单:Hex(UTF-8) + %

  1. 字符集确定:现代 Web 浏览器默认使用 UTF-8 字符集。
  2. 转字节:将字符转换为其对应的 16 进制字节序列。
  3. 加前缀:在每个字节前加上 %

以字符 < 为例:

  • ASCII 码值:60
  • 16 进制:3C
  • 编码结果:%3C

以中文 “测” 为例:

  • UTF-8 字节流:E6 B5 8B
  • 编码结果:%E6%B5%8B

4. 编码与解码的生命周期

理解了这个生命周期,你就会明白为什么 URL 编码防不住 XSS。

阶段动作执行者状态
1. 发起请求编码 (Encode)浏览器 / Axios / Fetchname=%3Cb%3E
2. 网络传输传输HTTP 协议 / 网线name=%3Cb%3E
3. 接收请求解码 (Decode)Web Server (Nginx/Tomcat/Node)name=<b>
4. 业务处理逻辑运行Controller / API拿到原始的 <b>

关键结论

后端框架(Spring Boot, Express, Django 等)在将请求交给你的业务代码(Controller)之前,通常已经自动完成了 URL 解码

如果你在数据库里存入了这个值,存进去的是 <b>,而不是 %3Cb%3E。当这个数据再次被取出来并渲染到页面上时,如果没做HTML 实体转义,浏览器就会执行它。

5. 前端开发实战指南

在 JS 中手动处理 URL 编码时,请务必区分这两个 API:

encodeURI vs encodeURIComponent

  • encodeURI:

    • 场景:编码整个 URL。
    • 特点:不会编码 : / ? & 等保留字符,确保 URL 结构不被破坏。
    • 错用后果:如果参数里包含 &,会导致参数解析截断。
  • encodeURIComponent (推荐):

    • 场景:编码 URL 中的参数值 (Query Value)
    • 特点:会编码 : / ? & 等所有非安全字符。
    • 最佳实践:拼接参数时永远使用它。
const param = "A&B"; // 参数值里带了 &

// ❌ 错误做法
const url1 = "http://api.com?q=" + encodeURI(param);
// 结果: ...?q=A&B  -> 后端解析为 q="A", 剩下的 B 成了无效部分

// ✅ 正确做法
const url2 = "http://api.com?q=" + encodeURIComponent(param);
// 结果: ...?q=A%26B -> 后端正确解析为 q="A&B"

6. 总结

  1. URL 编码是通信协议层面的规范,目的是让数据在网络传输中“合法”且“无歧义”。
  2. URL 编码不是安全防御。它在到达后端业务逻辑前就会被还原。
  3. 防御 XSS 的真正手段HTML 实体编码 (HTML Entity Encoding) (如将 < 转义为 &lt;),这通常由前端框架(Vue/React)在渲染层自动完成,或由 WAF 在网关层拦截。

不要被浏览器地址栏里的 % 迷惑,安全防御必须建立在对数据流转的深刻理解之上。