本文旨在厘清 URL 编码、HTML 实体、Base64、Unicode、十六进制的本质与边界,并从安全角度切入,理解各种编码的区别以及编码的防御原则

一、编码存在的根本原因
所有编码的设计出发点只有一个核心矛盾:
在特定场景下,某些字符拥有"特殊含义"。当你想把这些字符当作普通数据传递时,就需要编码来进行转义,让数据和控制符号区分开来。
这些编码并非由同一个团队在同一时间设计,而是不同时代、不同团队面对不同问题时各自发明的解法,因此才形成了今天这套看似繁杂的体系。
二、五种编码详解
1. URL 编码(百分号编码)
设计场景: HTTP 传输层
URL 本身有保留字符用于划分结构,? & = # / 各有语义。当数据本身包含这些字符时,就需要编码:
正常 URL:
?q=hello&page=1
如果搜索词含有 &,不编码:
?q=hello&world ← 服务端误解为两个参数
编码后:
?q=hello%26world ← %26 是纯数据,不再有分隔含义
空格有两种编码方式,适用场景不同:
| 编码 | 含义 | 适用场景 |
|---|---|---|
+ |
空格 | 查询字符串(表单编码规范) |
%20 |
空格 | URL 路径(RFC 3986 规范) |
%2B |
加号本身 | 区别于空格的+ |
设计目的: 让数据安全通过 URL 结构,不干扰 URL 语法。
2. HTML 实体编码
设计场景: 浏览器渲染层
HTML 解析时,< > & " ' 是结构符号。如果内容本身含有这些字符,需要转义:
<!-- 原始内容:3 < 5 是正确的 -->
<!-- 不转义 → 浏览器解析出错 -->
<p>3 < 5 是正确的</p>
<!-- 转义后 → 正常渲染 -->
<p>3 < 5 是正确的</p>
常用 HTML 实体对照:
| 原字符 | 实体编码 |
|---|---|
< |
< |
> |
> |
& |
& |
" |
" |
' |
' |
设计目的: 让数据安全嵌入 HTML,不被解析为标签结构。这也是防御 XSS 的核心机制。
3. Base64 编码
设计场景: 二进制数据通过纯文本信道传输
早期 Email 协议只支持 ASCII 纯文本,无法传输图片、附件等二进制数据。Base64 把任意二进制映射到 64 个可打印字符(A-Z、a-z、0-9、+、/):
原始: 你好
UTF-8: E4 BD A0 E5 A5 BD
Base64: 5L2g5aW9 ← 纯 ASCII,可安全传输
设计目的: 让二进制数据在只支持文本的信道中安全传输。
4. Unicode / UTF-8
设计场景: 全球多语言字符统一表示
ASCII 只有 128 个字符,各国各自开发编码标准(GBK、Shift-JIS 等),相互不兼容。Unicode 为世界上所有字符分配唯一码点,UTF-8 是其中一种变长存储方式:
'A' → U+0041 → UTF-8: 1字节 (0x41)
'你' → U+4F60 → UTF-8: 3字节 (0xE4 0xBD 0xA0)
'😀' → U+1F600 → UTF-8: 4字节 (0xF0 0x9F 0x98 0x80)
设计目的: 统一全球字符集,解决多语言乱码问题,不是为了传输安全。
5. 十六进制(0x)
设计场景: 数据库、编程语言的底层数据表示
十六进制是二进制的紧凑表示形式:1 字节 = 8 位二进制 = 2 位十六进制。在 MySQL 中可直接表示字节序列:
0x61646d696e = 'admin'
-- 直接表示字节序列,不需要引号
设计目的: 底层数据的直接表示,是编程和数据库的基础语法。
三、各层编码对照总览
层级 编码类型 解码方式
─────────────────────────────────────────────────────
浏览器渲染层 HTML 实体 < & 浏览器渲染时自动处理
HTTP 传输层 URL 编码 %27 %23,+ Web服务器 / PHP 自动解码
应用层 Base64 需显式调用 base64_decode()
数据库层 十六进制 0x,char() MySQL 内部识别
操作系统层 UTF-8 / Unicode 透明处理,开发者无感知
核心规律:每一层只负责解码自己的编码,跨层的编码不会被自动处理。
四、从安全角度看编码
编码本身是中性的技术机制,但在攻防场景中,它成为了绕过过滤和检测的重要手段。
编码不等于安全
一个常见误区是认为 URL 编码能防止 SQL 注入。实际上:
攻击者输入: admin' or 1=1 #
浏览器编码: admin%27+or+1%3D1+%23
PHP 解码: admin' or 1=1 # ← 还原成原始恶意字符串
SQL 拼接: WHERE username = 'admin' or 1=1 #'
URL 编码是传输格式,不是安全机制。 服务端自动解码后,攻击 payload 原样还原,对 SQL 注入毫无防御效果。
双重编码绕过过滤
正常编码: ' → %27 服务端解码一次 → ' 被过滤拦截
双重编码: ' → %27 → %2527 服务端只解码一次 → %27(字符串,非引号)
过滤器看到 %27,认为安全,放行
但某些框架会二次解码,最终变成 '
数据库层十六进制绕过 WAF
-- WAF 过滤了单引号,但没过滤十六进制:
SELECT * FROM users WHERE username = 'admin' -- 被拦截
SELECT * FROM users WHERE username = 0x61646d696e -- 绕过
各编码的安全风险汇总
| 编码类型 | 安全风险 | 典型攻击场景 |
|---|---|---|
| URL 编码 | 双重编码绕过 WAF | %2527→%27→' |
| HTML 实体 | 未转义导致 XSS | 输出<script> 未转义时执行 |
| Base64 | 混淆 payload | eval(atob("...")) 绕过关键字检测 |
| 十六进制 | 绕过引号过滤 | 0x61646d696e 替代 'admin' |
| Unicode | 同形字混淆 | Admin(全角 A)绕过用户名检测 |
防御的核心原则
攻击者利用的正是跨层传递时各层解码行为不一致——过滤在 A 层做了,但攻击 payload 在 B 层才展开。
错误的防御思路:
过滤输入中的 ' → 攻击者用 %27 或 0x27 绕过
正确的防御思路:
在数据被使用的那一层进行参数化处理
SQL 层 → Prepared Statement(预编译语句)
HTML 层 → htmlspecialchars() 转义输出
文件路径 → 白名单验证
总结
编码不是由某一个人统一设计的,而是各个时代的工程师面对各自问题时独立发明的解法。URL 编码解决 HTTP 传输冲突,HTML 实体解决渲染冲突,Base64 解决二进制传输问题,Unicode 统一全球字符,十六进制是底层的自然表达。
从安全角度看,每一种编码都是一把双刃剑:它解决了数据与语法的冲突问题,但也为攻击者提供了绕过过滤的手段。理解每种编码属于哪一层、由谁解码、何时展开,是做好 Web 安全防御的基础。
版权声明:本文采用 CC BY-NC-SA 4.0 协议授权,转载请注明出处并保留原始链接。
原文链接:https://www.jerrygao.cn//blog/E7BC96E7A081E79A84E8AEBEE8AEA1E593B2E5ADA6E4BB8EE4BCA0E8BE93E588B0E5AE89E585A8
评论 0
还没有评论,成为第一个留言的人吧!
