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.go b/lib/anubis.go index a3b93387..16142794 100644 --- a/lib/anubis.go +++ b/lib/anubis.go @@ -67,14 +67,15 @@ var ( ) type Server struct { - next http.Handler - mux *http.ServeMux - policy *policy.ParsedConfig - OGTags *ogtags.OGTagCache - ed25519Priv ed25519.PrivateKey - hs512Secret []byte - opts Options - store store.Interface + next http.Handler + mux *http.ServeMux + policy *policy.ParsedConfig + OGTags *ogtags.OGTagCache + ed25519Priv ed25519.PrivateKey + hs512Secret []byte + opts Options + store store.Interface + internalPath string } func (s *Server) getTokenKeyfunc() jwt.Keyfunc { 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..3ebdd5a0 100644 --- a/lib/config.go +++ b/lib/config.go @@ -101,13 +101,14 @@ func New(opts Options) (*Server, error) { anubis.BasePrefix = opts.BasePrefix result := &Server{ - next: opts.Next, - ed25519Priv: opts.ED25519PrivateKey, - hs512Secret: opts.HS512Secret, - policy: opts.Policy, - opts: opts, - OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph, opts.Policy.Store), - store: opts.Policy.Store, + next: opts.Next, + ed25519Priv: opts.ED25519PrivateKey, + hs512Secret: opts.HS512Secret, + policy: opts.Policy, + opts: opts, + OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph, opts.Policy.Store), + store: opts.Policy.Store, + internalPath: opts.BasePrefix + anubis.StaticPath, } mux := http.NewServeMux() @@ -154,7 +155,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..9b2fbe5c 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, s.internalPath) { + case true: + s.mux.ServeHTTP(w, r) + case false: + s.maybeReverseProxyOrPage(w, r) + } } func (s *Server) stripBasePrefixFromRequest(r *http.Request) *http.Request {