1. Root Cause:
未經清理的 Path 建構

Self-hosted Git 平台 軟體供應鏈 中處於高度特權的位置:它們管理儲存庫(Repository)的檔案系統樹(Filesystem tree)、代表不受信任的輸入執行 git binary,並經常執行會觸發任意 shell 指令的 hooks。這份報告分析了開源 Git 託管應用程式 Gogs 中最近披露的漏洞鏈、漏洞鏈編號為 CVE-2026-52813 的開源 Git Hosting Application 漏洞鏈,該漏洞可將 organization-name path traversal 提升為完整的 遠端程式碼執行(Remote Code Execution, RCE) ;同時也分析另一個伴隨的 邏輯漏洞 CVE-2026-52810,該漏洞混淆了 Git Smart HTTP protocol 的 pull/push 授權模型 [1] 。以下分析僅限於原始揭露內容中的 source excerpts,並以 sequence diagrams 重建 exploitation flow,而不重現 screenshots。

你以為只是 Git Hook? Gogs 命名空間混淆背後隱藏的 RCE 邏輯漏洞 | 資訊安全新聞

1. Root Cause:未經清理的 Path 建構

平台透過單一 helper 解析任何帳戶儲存庫在磁碟上的位置,該 helper 會將設定的 repository root 與轉為小寫的帳戶名稱進行 join。一般使用者在註冊時受到嚴格的字元類別限制,因此 directory-traversal 序列無法進入這個 join。然而,Organization 是透過另一個 API request object 建立,該物件省略了相同的 binding 限制,因此同一個 join operation 會接收到完全由攻擊者控制的輸入。

  1. // repox.go — path resolver shared by users AND organizations
  2. // filepath.Join performs lexical normalization of ".." segments,
  3. // so it does NOT act as a containment boundary; it merely
  4. // concatenates and cleans the path.
  5. func UserPath(user string) string {
  6. return filepath.Join(conf.Repository.Root, strings.ToLower(user))
  7. }
  8. // Register (used for normal user signup) is constrained by a
  9. // binding tag: only letters, digits and "-_." are accepted, so
  10. // "../" can never appear in a registered username.
  11. type Register struct {
  12. UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"`
  13. Email string `binding:"Required;Email;MaxSize(254)"`
  14. Password string `binding:"Required;MaxSize(255)"`
  15. }
  16. // createOrgRequest (used for organization creation) has NO such
  17. // character-class restriction on UserName — only "Required" is
  18. // enforced. This asymmetry is the actual root cause: the same
  19. // UserPath() sink is reachable through a path that skips the
  20. // sanitizing gate applied to the sibling type.
  21. type createOrgRequest struct {
  22. UserName string `json:"username" binding:"Required"`
  23. FullName string `json:"full_name"`
  24. }

由於 Organization 名稱會被相同的 UserPath() sink 使用,因此將 ../../../../tmp/test 作為 Organization 名稱送出時,會解析到預期 repository root 之外的位置,而之後在該 Organization 底下建立的任何 repository,都會以 bare Git repository 的形式寫入攻擊者指定的目錄(僅包含 metadata,沒有 working tree) [1] 。這反映出 Go 漏洞中更廣泛的一類問題:將 filepath.Join 誤認為 jail——類似模式也出現在 2026 年一項影響 local-development tool 的揭露中,其 archive extraction routine 使用 filepath.Join(dest, file.Name) ,卻沒有對 archive entries 執行 post-join containment check [2]

2. 從 Weak Write Primitive 到任意程式碼執行

單獨的 bare repository 是一個 weak primitive:攻擊者無法任意選擇其中的檔案名稱,只能使用固定的 Git metadata layout。已公開的研究透過攻擊平台如何為其網頁檔案編輯器建立實際 working tree 來進一步提升攻擊效果;平台會暫時將其 checkout 到一個可預測、並以目標 repository 的 numeric ID 為索引的 local Path。透過選擇一個會 traversal 到該確切 worktree directory 的 Organization 名稱,攻擊者的 path traversal 可以將 bare repository 放置在合法 worktree 內部 ,使其中的 hooks/ directory 可以透過一般 Git operations 進行修改。

sequenceDiagram participant A as Attacker (API client) participant S as Git Server (App Logic) participant FS as Filesystem A->>S: POST /api/v1/user/orgs {username: "../../gogs/data/tmp/local-r/1"} S->>FS: UserPath(org) = root + traversal sequence Note over FS: Resolves inside victim repo's live worktree A->>S: POST .../repos {name: "traversal"} S->>FS: create bare repo "traversal.git" (metadata only) A->>S: PUT contents/dummy (trigger UI sync) S->>FS: refresh worktree listing Note over A,S: UI editor blocks paths containing ".git/"
so traversal.git/hooks/update cannot be edited via API A->>S: git clone editor.git (native protocol, no UI filter) A->>A: append "id>/tmp/pwned" to traversal.git/hooks/update A->>S: git push (commit touches hooks/update) A->>S: PUT contents/dummy4 on org/traversal (trigger internal commit) S->>FS: internal push into nested traversal.git FS-->>S: Git invokes hooks/update S-->>FS: shell executes attacker payload as service account

攻擊流程:Organization 名稱的 path traversal 與 web editor 的 worktree 機制及 native Git push 串接,進而觸發 malicious update hook。

唯一的障礙是 UI 端的 guard。該 guard 會拒絕任何包含 .git/ 區段的編輯 Path,其目的在於阻止透過檔案編輯器直接操作 repository metadata。

  1. // editor.go — server-side guard against editing inside ".git"
  2. func isRepositoryGitPath(path string) bool {
  3. path = strings.ToLower(path)
  4. return strings.HasSuffix(path, ".git") ||
  5. strings.Contains(path, ".git/") ||
  6. strings.Contains(path, `.git\`) ||
  7. // Windows treats ".git." the same as ".git"
  8. strings.HasSuffix(path, ".git.") ||
  9. strings.Contains(path, ".git./") ||
  10. strings.Contains(path, `.git.\`)
  11. }

這項檢查只有在 Path 區段 完全 .git 時才會觸發;刻意命名為 traversal.git 的 nested repository 不符合上述任何 substring,因此只要選擇一個名稱不是字面上的 .git 的 bare-repository name,就能繞過這項 guard。由於這個 filter 只套用在 web editor 的 HTTP handler,而沒有套用到 native Git smart-protocol push Path,因此攻擊者可以 clone 外層 repository,在 local 端修改 traversal.git/hooks/update ,再進行 push,完全繞過這項檢查。之後對 nested repository 觸發 internal commit,會使 Git 呼叫遭到植入惡意內容的 update hook,最終以 service account 身分執行指令 [1]

3. Protocol-Level Authorization Confusion(CVE-2026-52810)

第二個漏洞與 Path handling 無關,而是利用 authorization middleware 與最終 dispatch request 的 router 之間不一致的 request parsing。Smart HTTP protocol 暴露兩個邏輯 operation—— git-upload-pack (pull)與 git-receive-pack (push)——middleware 同時透過 query parameter 與 URL Path 進行分類,而 router 後續則只透過 regular expressions 對 URL Path 進行比對。

  1. // http.go — authorization classification (middleware)
  2. // isPull decides Read vs Write access mode for the whole request.
  3. isPull := c.Query("service") == "git-upload-pack" ||
  4. strings.HasSuffix(c.Req.URL.Path, "git-upload-pack") ||
  5. c.Req.Method == "GET"
  6. // NOTE: an attacker-supplied "?service=git-upload-pack" query
  7. // string is trusted even on a path that the router will later
  8. // treat as a receive-pack (write) endpoint.
  9. mode := database.AccessModeWrite
  10. if isPull {
  11. mode = database.AccessModeRead
  12. }

由於 router 的 endpoint dispatch 不會參考 service query parameter,因此對 /git-receive-pack Path 發送一個帶有 ?service=git-upload-pack 的 request 時,該 request 會以 read 的身分取得授權,但實際執行時卻會被當成 write endpoint。要利用這項問題,只需要一個輕量級 proxy,在 Git 自己的 push logic 發出 request 前,修改 outgoing push requests 並附加具有誤導性的 query parameter,即可有效地讓 authorization 將 write 降級為 read。

sequenceDiagram participant U as Attacker (read-only collaborator) participant P as Rewrite Proxy participant S as Git Server U->>P: git push (native Smart HTTP) P->>S: POST /repo/git-receive-pack?service=git-upload-pack S->>S: isPull=true (query says "upload-pack") → AccessModeRead Note over S: Read is sufficient for a collaborator S->>S: Router matches path suffix "git-receive-pack" → serviceReceivePack handler S-->>U: push objects accepted into repository

authorization middleware(由 query 驅動)與 routing layer(由 Path 驅動)之間的混淆,使 read-only account 能夠存取 write handler。

在 public repositories 上還會出現一個有趣的副作用:middleware 對 anonymous pulls 有一個 early return,會完全跳過 authenticated-user context 的填入。由於 confusion 迫使這個 branch 即使在語意上是 push 的情況下也會被採用,因此 downstream write handler 會取用一個從未設定的 user object:

  1. // http.go — early return taken when a request is misclassified as a pull
  2. // on a repository that does not require sign-in to read.
  3. if isPull && !repo.IsPrivate && !conf.Auth.RequireSigninView {
  4. c.Map(&HTTPContext{
  5. Context: c, // AuthUser intentionally left nil here
  6. })
  7. return
  8. }
  9. // repo_editor.go later assumes AuthUser is non-nil:
  10. // EnvAuthUserID + "=" + strconv.FormatInt(opts.AuthUser.ID, 10)
  11. // -> nil pointer dereference (crash) on fully public repos,
  12. // but a *successful* unauthorized write on private repos
  13. // or instances requiring sign-in to view, where this branch
  14. // is not taken and AuthUser is populated normally.

上游公開的 patch 將 classification 縮小到只依據 URL Path,但 patch 的 exact-match check 與 router 的 case-insensitive matching 之間仍存在大小寫處理落差(例如 git-RECEIVE-pack ),留下繞過方式。這說明針對 routing-layer confusion 所進行的 authorization 修補,必須與 router 本身的 normalization rules 一致,而不能只限制 keyword comparison [1]

4. Comparative Analysis

這種提權模式——由 weak sanitization 控制的 filesystem write primitive,再與無關元件串接以取得程式碼執行——會反覆出現在不同的裝置與基礎架構類別中。在 embedded browser-service component 中,一個同樣驗證不足的檔案 Path parameter 允許取得任意檔案,再與另一個 authentication-bypass 漏洞結合,最終取得完整的裝置控制權( 前期文章中對 WebOS path traversal 與 authentication bypass 串接的分析 )。其中的結構性教訓完全相同:單一 traversal primitive 本身很少具有致命性,但一旦與第二個各自看似不嚴重的漏洞結合,就可能成為關鍵問題。類似模式也出現在 cluster-admission infrastructure 中,一個驗證不足的 filename 被送入 directory-traversal condition;雖然單獨來看嚴重性較低,但與另一個 distinct injection flaw 結合後,便成為更廣泛攻擊鏈的墊腳石( 前期文章中對 Ingress-Nginx admission-controller vulnerability chain 的分析 )。這兩個案例都再次證明,缺乏明確 post-join containment check 的 filepath.Join -style sinks,如前述 archive-extraction 案例所記錄 [2] ,在不同語言與產品類別中仍是反覆出現的 Root Cause,與具體的 application domain 無關。

5. Conclusion

Gogs 的這個案例顯示,一個未經清理、並送入 path-join operation 的 string field,在應用程式同時提供 editor-worktree 功能與 native Git hook execution 時,可以一路演變成 remote code execution。另一方面,authorization-confusion 漏洞也顯示,分層的 request classification——一個元件讀取 query parameter,而另一個元件只讀取 URL——如果每一層沒有以完全一致的方式進行 input normalization,就天生十分脆弱。因此,有效的修補方式必須在所有能夠抵達共用 filesystem sink 的 entry points 套用相同的 character-class constraints,並將 protocol classification 集中到單一 authoritative function,讓 authorization 與 routing logic 都使用相同的分類結果。