diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 407841ce..348abde3 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -99,6 +99,7 @@ There are a bunch of other assorted features and fixes too: - Allow [Common Crawl](https://commoncrawl.org/) by default so scrapers have less incentive to scrape - The [bbolt storage backend](./admin/policies.mdx#bbolt) now runs its cleanup every hour instead of every five minutes. - Don't block Anubis starting up if [Thoth](./admin/thoth.mdx) health checks fail. +- Multiple consecutive slashes are supported in upstream application URLs ([#754](https://github.com/TecharoHQ/anubis/issues/754)). ### Potentially breaking changes diff --git a/lib/anubis_test.go b/lib/anubis_test.go index 7ed0426f..eb070c18 100644 --- a/lib/anubis_test.go +++ b/lib/anubis_test.go @@ -204,6 +204,63 @@ func TestCVE2025_24369(t *testing.T) { } } +func TestDoubleSlashes(t *testing.T) { + pol := loadPolicies(t, "", 0) + + path := "" + + srv := spawnAnubis(t, Options{ + Next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path = r.URL.Path + }), + Policy: pol, + }) + + ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv)) + defer ts.Close() + + cli := httpClient(t) + chall := makeChallenge(t, ts, cli) + resp := handleChallengeZeroDifficulty(t, ts, cli, chall) + + if resp.StatusCode != http.StatusFound { + t.Fatal("can't solve challenge, see logs") + } + + for _, tt := range []struct { + name, path string + }{ + { + name: "basic", + path: "/foo", + }, + { + name: "leading slashes", + path: "//foo", + }, + { + name: "mid slashes", + path: "/foo//bar///baz", + }, + { + name: "trailing slashes", + path: "/foo/bar///", + }, + } { + t.Run(tt.name, func(t *testing.T) { + if _, err := cli.Get(ts.URL + tt.path); err != nil { + t.Errorf("can't make request to %s: %v", tt.path, err) + } + + if path != tt.path { + t.Logf("want: %s", tt.path) + t.Logf("got: %s", path) + t.Error("invalid path sent to server") + } + }) + } +} + func TestCookieCustomExpiration(t *testing.T) { pol := loadPolicies(t, "", 0) ckieExpiration := 10 * time.Minute diff --git a/lib/config.go b/lib/config.go index 9c6708f3..9eab1a33 100644 --- a/lib/config.go +++ b/lib/config.go @@ -154,7 +154,6 @@ func New(opts Options) (*Server, error) { registerWithPrefix(anubis.APIPrefix+"pass-challenge", http.HandlerFunc(result.PassChallenge), "GET") registerWithPrefix(anubis.APIPrefix+"check", http.HandlerFunc(result.maybeReverseProxyHttpStatusOnly), "") - registerWithPrefix("/", http.HandlerFunc(result.maybeReverseProxyOrPage), "") //goland:noinspection GoBoolExpressions if anubis.Version == "devel" { diff --git a/lib/http.go b/lib/http.go index 7c59c396..46f97e37 100644 --- a/lib/http.go +++ b/lib/http.go @@ -200,7 +200,12 @@ func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg s } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - s.mux.ServeHTTP(w, r) + switch strings.HasPrefix(r.URL.Path, anubis.StaticPath) { + case true: + s.mux.ServeHTTP(w, r) + case false: + s.maybeReverseProxyOrPage(w, r) + } } func (s *Server) stripBasePrefixFromRequest(r *http.Request) *http.Request {