必须只调用一次url.PathUnescape,多次解码会将%252e%252e还原为..触发路径遍历;它不幂等处理多重编码,合法路径不应含多层编码,疑似多重编码应直接拒绝而非尝试修复。

url.PathUnescape 只能解码一次,多次调用不仅无效,反而可能引入双重解码绕过漏洞——比如 %252e%252e(即 %2e%2e 的二次编码)经两次 url.PathUnescape 会变成 ..,直接触发路径遍历。
为什么不能多次 url.PathUnescape
Go 的 url.PathUnescape 是幂等解码:对已解码字符串再次调用,会返回原字符串(无错误),但对双重编码输入,它会“忠实”还原一层。攻击者正利用这点构造嵌套编码绕过校验。标准库不提供“递归解码”或“深度解码”接口,因为这本身就不安全——合法 URL 路径不该含多层编码。
url.PathUnescape 必须且仅调用一次
解码时机和顺序决定安全性:
- 只对来自 HTTP 请求中需作路径拼接的字段调用,如
r.URL.Path已由 net/http 自动解码,**不要重复解码**;但r.URL.Query().Get("file")或表单字段必须显式调用url.PathUnescape - 解码后立即传给
filepath.Join,禁止先拼接再解码,更禁用+或fmt.Sprintf拼路径字符串 - 若输入疑似多重编码(如含
%25),说明上游已出错或被恶意构造,应直接拒绝,而不是尝试“修复”
真正防注入靠的是分层校验,不是反复解码
路径注入防御不是靠“解得够深”,而是靠各层职责分明:
-
第一层:解码 —— 仅一次
url.PathUnescape(input),拿到原始字节序列 -
第二层:拼接与标准化 —— 用
filepath.Join(root, unescaped),再用filepath.Clean归一化(注意:它不防越界,只做路径规整) -
第三层:越界检查 ——
rel, err := filepath.Rel(root, cleanedPath),若 err != nil 或 rel 以..开头,说明已跳出白名单目录 -
第四层:符号链接与竞态防护 —— 用
os.Lstat替代os.Stat,避免跟随 symlink;且在 open 前一刻才 stat,减少 TOCTOU 窗口
常见错误:把 url.QueryUnescape 当 url.PathUnescape 用
两者语义不同,混用会导致解码失败或语义错乱:
立即学习“go语言免费学习笔记(深入)”;
-
url.QueryUnescape("a+b")→"a b"(+ 变空格),但路径中不会出现 + 表示空格 -
url.PathUnescape("a%20b")→"a b"(%20 变空格),且保留 / 不转义 - 若对路径段误用
QueryUnescape,遇到%2f会被解成/,导致路径结构被意外拆分
filepath.Join、越界用 filepath.Rel、链接用 os.Lstat、扩展名和隐藏文件用白名单过滤,缺一不可。


















