From 2ea82966823d42e00c36d286e57c16ec2229d414 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Sat, 12 Jul 2025 17:57:32 +0000 Subject: [PATCH 1/2] feat(lib): enable multiple consecutive slash support Replaces #808 Closes #754 Some web applications require the ability to include multiple consecutive slashes in a URL. This could be for optional path variables or for wiki article titles that start with a leading slash. I wasn't aware that the RFC allowed this. Signed-off-by: Xe Iaso --- docs/docs/CHANGELOG.md | 1 + lib/anubis_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++ lib/config.go | 1 - lib/http.go | 7 +++++- 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 407841ce0..348abde3d 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 7ed0426ff..eb070c189 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 9c6708f3d..9eab1a33b 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 7c59c396d..46f97e37a 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 { From d6f02ac5f905dc4672cd72fdf6160b60376e0a8b Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Sat, 12 Jul 2025 19:07:19 +0000 Subject: [PATCH 2/2] fix(lib): try to make base path prefixing work again Signed-off-by: Xe Iaso --- lib/anubis.go | 17 +++++++++-------- lib/config.go | 15 ++++++++------- lib/http.go | 2 +- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/anubis.go b/lib/anubis.go index a3b933871..16142794b 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/config.go b/lib/config.go index 9eab1a33b..3ebdd5a08 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() diff --git a/lib/http.go b/lib/http.go index 46f97e37a..9b2fbe5c5 100644 --- a/lib/http.go +++ b/lib/http.go @@ -200,7 +200,7 @@ func (s *Server) respondWithStatus(w http.ResponseWriter, r *http.Request, msg s } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - switch strings.HasPrefix(r.URL.Path, anubis.StaticPath) { + switch strings.HasPrefix(r.URL.Path, s.internalPath) { case true: s.mux.ServeHTTP(w, r) case false: