bigywater.tech
Gin · 18

UseRawPath 与 UnescapePathValues

路径里有 %2F 的 URL,Gin 默认匹配不了 /files/:name。打开 UseRawPath 能解决,但会带来什么?这篇把三个开关的组合、哪些字符受影响、取到的值长什么样都列出来,并验证打开它之后的路径穿越风险。

404 → "a/b"
UseRawPath=true 且 Unescape=true 时 /files/a%2Fb 的结果(默认是 404)
"a b"
UseEscapedPath 且 Unescape 时 /files/a+b 的 name,加号变成了空格
../../etc/passwd
UseRawPath=true 时 /dl/..%2F..%2Fetc%2Fpasswd 的 name

三个开关与 RawPath

实测 · 本机执行输出
  gin.go 的 handleHTTPRequest 开头(Gin v1.12.0):
     690 | func (engine *Engine) handleHTTPRequest(c *Context) {
     691 |     httpMethod := c.Request.Method
     692 |     rPath := c.Request.URL.Path
     693 |     unescape := false
     694 | 
     695 |     if engine.UseEscapedPath {
     696 |         rPath = c.Request.URL.EscapedPath()
     697 |         unescape = engine.UnescapePathValues
     698 |     } else if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
     699 |         rPath = c.Request.URL.RawPath
     700 |         unescape = engine.UnescapePathValues
     701 |     }
     702 | 
     703 |     if engine.RemoveExtraSlash {
     704 |         rPath = cleanPath(rPath)
     705 |     }
     706 | 
    默认:UseRawPath=false UseEscapedPath=false UnescapePathValues=true RemoveExtraSlash=false
    /files/a%2Fb       URL.Path="/files/a/b"   URL.RawPath="/files/a%2Fb"   EscapedPath()="/files/a%2Fb"
    /files/a%20b       URL.Path="/files/a b"   URL.RawPath=""               EscapedPath()="/files/a%20b"
    /files/%E4%B8%AD   URL.Path="/files/中"     URL.RawPath=""               EscapedPath()="/files/%E4%B8%AD"
    /files/a+b         URL.Path="/files/a+b"   URL.RawPath=""               EscapedPath()="/files/a+b"
    /files/100%25      URL.Path="/files/100%"  URL.RawPath=""               EscapedPath()="/files/100%25"
    /files/a%2fb       URL.Path="/files/a/b"   URL.RawPath="/files/a%2fb"   EscapedPath()="/files/a%2fb"
    /files/%7Euser     URL.Path="/files/~user" URL.RawPath="/files/%7Euser" EscapedPath()="/files/%7Euser"
    /files/a%3Fb       URL.Path="/files/a?b"   URL.RawPath=""               EscapedPath()="/files/a%3Fb"

Gin 默认用已经解码的 URL.Path 去匹配路由,所以 %2F 会变成真的斜杠。URL.RawPath 只有在“默认的编码方式与原始写法不同”时才被 net/url 填上:%2F、%7E 会有,%20、中文、%25、+、%3F 都是空串。所以 UseRawPath 对后面这些写法没有任何作用。

5 种组合下的取值

实测 · 本机执行输出
                                   a%2Fb             a%20b             %E4%B8%AD         a+b               100%25            a%2Fb/c
    默认                             "404"             "a b"             "中"               "a+b"             "100%"            "404"
    Raw=true Unescape=true         "a/b"             "a b"             "中"               "a+b"             "100%"            "a/b|c"
    Raw=true Unescape=false        "a%2Fb"           "a b"             "中"               "a+b"             "100%"            "a%2Fb|c"
    Escaped=true Unescape=true     "a/b"             "a b"             "中"               "a b"             "100%"            "a/b|c"
    Escaped=true Unescape=false    "a%2Fb"           "a%20b"           "%E4%B8%AD"       "a+b"             "100%25"          "a%2Fb|c"

只有 UseRawPath 或 UseEscapedPath 才能让 %2F 留在同一段里。UnescapePathValues=true 时取出的值再被解码(a%2Fb → a/b),关掉则保持转义形式。一个容易忽略的差别:UseEscapedPath=true 加 Unescape 时用的是 url.QueryUnescape,所以 a+b 会变成 a b——这是查询串的规则,不是路径的规则。

通配符里的 %2F

实测 · 本机执行输出
    UseRawPath=false UnescapePathValues=false:
      /s/a%20b       → 200 "/a b"
      /s/a%2Fb/c     → 200 "/a/b/c"
      /s/%E4%B8%AD   → 200 "/中"
      /s/a%25b       → 200 "/a%b"
    UseRawPath=true UnescapePathValues=false:
      /s/a%20b       → 200 "/a b"
      /s/a%2Fb/c     → 200 "/a%2Fb/c"
      /s/%E4%B8%AD   → 200 "/中"
      /s/a%25b       → 200 "/a%b"

*p 也一样:默认情况 /s/a%2Fb/c 的 p 是 /a/b/c(已经分不清原来有没有编码);只有 UseRawPath=true 且不解码时才能保留 /a%2Fb/c,后面的代码可以自己区分两种斜杠。

安全边界

实测 · 本机执行输出
    [默认]
      /dl/report.pdf               → 200 name="report.pdf" Join(/srv/files, name)="/srv/files/report.pdf"
      /dl/..%2F..%2Fetc%2Fpasswd   → 404 404 page not found
      /dl/%2e%2e%2fsecret          → 404 404 page not found
    [UseRawPath=true]
      /dl/report.pdf               → 200 name="report.pdf" Join(/srv/files, name)="/srv/files/report.pdf"
      /dl/..%2F..%2Fetc%2Fpasswd   → 200 name="../../etc/passwd" Join(/srv/files, name)="/etc/passwd"
      /dl/%2e%2e%2fsecret          → 200 name="../secret" Join(/srv/files, name)="/srv/secret"

默认配置里 ..%2F 因为被拆成多段,碰巧进不了 :name。打开 UseRawPath 之后,:name 的值是 ../../etc/passwd,直接拼进文件路径就是路径穿越。打开这个开关的同时,必须对参数做白名单或 filepath.Base 之类的清理。

分步演示

UseRawPath 与转义
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。请求用 httptest.NewRequest 构造(没有经过真实的 http.Server 与 http.ServeMux);net/url 的 RawPath 行为来自 Go 1.27.1 的标准库;每种情形各一次
    • 没有验证:真实浏览器和 curl 对 %2F 的预处理(许多反向代理会拒绝或改写 %2F,如 nginx 的 merge_slashes、proxy_pass 的处理);UseRawPath 与静态文件服务(Static)的组合;其它特殊字符(分号、反斜杠)在各开关下的表现;Gin 其他版本的行为
    京ICP备2026067502号-1京公网安备11010502063487号