1. 惊魂一刻:被悄无声息绕过的 WAF
几个月前,我的一个安全圈朋友遭遇了一次严重的事故:客户的数据库被删了。
最离谱的是,客户花大价钱部署了业界顶级的 WAF(Web Application Firewall,Web 应用防火墙)。安全团队在审计日志时发现,黑客使用的 payload 其实是非常基础的 SQL 注入语句:
' OR 1=1 --
按理说,任何一个及格的 WAF 都能在一毫秒内拦截这种教科书级别的特征串。但黑客发过来的 HTTP 请求,长这个样子:
https://api.example.com/getUser?id=%2527%2520OR%25201%253D1%2520--
这条看似毫无特征、连一个单引号都找不到的 URL,如同幽灵一般穿透了防火墙,直捣数据库黄龙。 它的名字叫:双重 URL 编码绕过(Double URL Encoding Bypass)。
2. 拨云见日:什么是双重编码?
在讲漏洞之前,我们先复习一下 URL 编码(Percent-encoding)的本质。
因为 URL 规范严格限制了可使用的字符集(主要为字母、数字和少许符号),任何特殊字符(如空格、中文字符、引号等)都必须被转义。规则很简单:% 加上该字符 ASCII 码的两位十六进制表示。
- 空格 ->
%20 - 单引号
'->%27
那什么是双重编码?
就是对“已经编码过的结果”再进行一次编码。
以单引号 ' 为例:
- 第一次编码:
'变成了%27 - 第二次编码:对
%27进行编码。注意,这里的%也是一个特殊字符,它的编码是%25。所以,%27就变成了%2527。
3. 攻击链还原:一次完美的击杀
黑客是如何利用 %2527 完成击杀的?这就涉及到现代 Web 架构中,不同组件对解码规则的认知差异(我们称之为“盲人摸象”)。
步骤 1:WAF 的浅尝辄止
流量首先到达 WAF。WAF 的核心工作原理是“解码然后匹配规则”。
面对 id=%2527%2520OR%25201%253D1%2520--,大多数 WAF 为了保证极高的吞吐性能,只会进行一次解码。
一次解码后,字符串变成了:%27 %20OR%201%3D1 %20--
WAF 拿着这个字符串去对比特征库:没有单引号!没有敏感关键字(它们被切碎了)!判定为:安全,放行。
步骤 2:Web 框架的自作主张
流量穿过 WAF,来到了业务服务器(比如一台跑着 Express / Spring Boot 的服务器)。
现代 Web 框架在解析 Query String 时,通常会自动做一次 URL 解码,以便提取出参数交给业务代码。
此时,%27 %20OR%201%3D1 %20-- 被框架自动解码,变成了致命的原始形态:
' OR 1=1 --
步骤 3:致命的拼接
业务代码从框架中拿到了参数 id(此时已经是原始的恶意 SQL 片段了),然后执行了最不可原谅的代码——字符串拼接:
// 灾难发生的地方
const sql = "SELECT * FROM users WHERE id = '" + req.query.id + "'";
db.execute(sql);
由于 id 包含了一个单引号,它提前闭合了原本的 SQL 语句,导致后面的 OR 1=1 恒成立,整张表的数据就这样暴露了。
4. 深层原因:过度解码与层级割裂
其实,这套攻击链能成功的核心原因不仅在于 WAF 的疏漏,更在于开发者在代码中滥用了 decodeURIComponent。
很多前端或 Node.js 开发者在接到乱码参数时,第一反应就是盲目地 decodeURIComponent() 一下。如果框架已经帮你解过一次码,你再解一次,这等同于你在应用层主动制造了一个“二次解码漏洞”。这种行为在安全审计中被称为“Self-inflicted Wound(自残式漏洞)”。
5. 其他的 URL 安全变种
双重编码不仅能用来 SQL 注入,它是打通各类漏洞的“万能钥匙”:
- 目录穿越(Path Traversal):用
%252e%252e%252f代替../,绕过 Nginx 的目录限制,读取服务器的/etc/passwd。 - 反射型 XSS:将
<script>双重编码为%253Cscript%253E,绕过浏览器的原生 XSS 过滤器。 - 三重甚至四重编码:有些极端情况下,攻击者会叠算多次编码,以应对链路层级异常复杂的微服务架构。
6. 防御指南:如何彻底封死双重编码
- 绝对禁止二次解码:明确你的框架规范,如果底层(如 Express/Koa)已经处理了解码,业务代码中 绝不 允许再次调用类似
decodeURIComponent的函数。 - 升级 WAF 策略:配置 WAF 进行递归解码(Recursive Decoding)。即一直解码,直到字符串不再发生变化为止(通常限制最大深度为 3 以防 DoS 攻击),然后再进行正则匹配。
- 参数化查询(Prepared Statements):这才是终极答案!不要拼接 SQL。只要使用了参数化查询,不管传进来的参数是双重编码还是十重编码,它都只会被当作单纯的字符串数据处理,永远不会变成可执行指令。
7. 结语
HTTP 协议的开放性和灵活度造就了繁荣的 Web,但也带来了无尽的攻防博弈。
在 daima.life 的 URL 编解码工具中,我们特意加入了一个小功能:当你点击解码时,如果系统检测到结果中仍然包含形如 %2X 的模式,它会提醒你“可能存在多重编码”。
理解编码机制,不仅是为了让页面正常显示,更是为了在恶意流量的洪流中,守住最后一道防线。