Commit ab95b83440
Verified · cmc
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 | ||
| 61 | const nextCookie = "gitbay_next" | 61 | const 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. | ||
| 65 | func 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. |
| 66 | func (s *Server) setNext(w http.ResponseWriter, path string) { | 72 | func (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 @@ | |||
| 1 | package httpd | ||
| 2 | |||
| 3 | import "testing" | ||
| 4 | |||
| 5 | func 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 | } | ||