Commit ab95b83440

ab95b83440f33e47b7735b71719db154c90e3ff3

parent: ed8f3378b1

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 02:15 UTC

httpd: the login return path refuses a leading /\

Ref #297

Layout: unified · split

internal/httpd/flash.go +9 −3
@@ -60,11 +60,17 @@ func (s *Server) reauthNotice(w http.ResponseWriter, notice, path string) bool {
60 60
61const nextCookie = "gitbay_next" 61const nextCookie = "gitbay_next"
62 62
63// localPath reports whether p is a path on this host. Browsers read a
64// leading `/\` like "//", so it is refused too.
65func localPath(p string) bool {
66 return strings.HasPrefix(p, "/") && !strings.HasPrefix(p, "//") && !strings.HasPrefix(p, "/\\")
67}
68
63// setNext remembers the local path an anonymous visitor asked for, so 69// setNext remembers the local path an anonymous visitor asked for, so
64// the login that follows can return there. Only a GET path is stored: 70// the login that follows can return there. Only a GET path is stored:
65// a POST must not be replayed. 71// a POST must not be replayed.
66func (s *Server) setNext(w http.ResponseWriter, path string) { 72func (s *Server) setNext(w http.ResponseWriter, path string) {
67 if !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") || len(path) > 300 { 73 if !localPath(path) || len(path) > 300 {
68 return 74 return
69 } 75 }
70 http.SetCookie(w, &http.Cookie{ 76 http.SetCookie(w, &http.Cookie{
@@ -83,7 +89,7 @@ func (s *Server) takeNext(w http.ResponseWriter, r *http.Request) string {
83 } 89 }
84 http.SetCookie(w, s.clearCookie(nextCookie, http.SameSiteLaxMode)) 90 http.SetCookie(w, s.clearCookie(nextCookie, http.SameSiteLaxMode))
85 p, err := url.QueryUnescape(c.Value) 91 p, err := url.QueryUnescape(c.Value)
86 if err != nil || !strings.HasPrefix(p, "/") || strings.HasPrefix(p, "//") { 92 if err != nil || !localPath(p) {
87 return "" 93 return ""
88 } 94 }
89 return p 95 return p
@@ -97,7 +103,7 @@ func (s *Server) peekNext(r *http.Request) string {
97 return "" 103 return ""
98 } 104 }
99 p, err := url.QueryUnescape(c.Value) 105 p, err := url.QueryUnescape(c.Value)
100 if err != nil || !strings.HasPrefix(p, "/") || strings.HasPrefix(p, "//") { 106 if err != nil || !localPath(p) {
101 return "" 107 return ""
102 } 108 }
103 return p 109 return p
internal/httpd/flash_test.go added +19
@@ -0,0 +1,19 @@
1package httpd
2
3import "testing"
4
5func TestLocalPath(t *testing.T) {
6 for p, want := range map[string]bool{
7 "/settings": true,
8 "/a/b?c=d": true,
9 "": false,
10 "settings": false,
11 "//evil.example": false,
12 `/\evil.example`: false,
13 "https://x.test/": false,
14 } {
15 if got := localPath(p); got != want {
16 t.Errorf("localPath(%q) = %v, want %v", p, got, want)
17 }
18 }
19}