diff --git a/VERSION b/VERSION index 141f2e8..ace4423 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.15.0 +1.15.1 diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 931ab9d..18513f5 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + - Added a periodic cleanup routine for the decaymap that removes expired entries, ensuring stale data is properly pruned. - Added a no-store Cache-Control header to the challenge page - Hide the directory listings for Anubis' internal static content @@ -28,6 +29,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a typo in the challenge page title. - Disabled running integration tests on Windows hosts due to it's reliance on posix features (see [#133](https://github.com/TecharoHQ/anubis/pull/133#issuecomment-2764732309)). +## v1.15.1 + +Zenos yae Galvus: Echo 1 + +Fixes a recurrence of [CVE-2025-24369](https://github.com/Xe/x/security/advisories/GHSA-56w8-8ppj-2p4f) +due to an incorrect logic change in a refactor. This allows an attacker to mint a valid +access token by passing any SHA-256 hash instead of one that matches the proof-of-work +test. + +This case has been added as a regression test. It was not when CVE-2025-24369 was released +due to the project not having the maturity required to enable this kind of regression testing. + ## v1.15.0 Zenos yae Galvus diff --git a/lib/anubis.go b/lib/anubis.go index 1b2ebfc..732d2c3 100644 --- a/lib/anubis.go +++ b/lib/anubis.go @@ -145,14 +145,13 @@ func New(opts Options) (*Server, error) { } type Server struct { - mux *http.ServeMux - next http.Handler - priv ed25519.PrivateKey - pub ed25519.PublicKey - policy *policy.ParsedConfig - opts Options - DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse] - ChallengeDifficulty int + mux *http.ServeMux + next http.Handler + priv ed25519.PrivateKey + pub ed25519.PublicKey + policy *policy.ParsedConfig + opts Options + DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse] } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -441,9 +440,9 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { } // compare the leading zeroes - if !strings.HasPrefix(response, strings.Repeat("0", s.ChallengeDifficulty)) { + if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) { s.ClearCookie(w) - lg.Debug("difficulty check failed", "response", response, "difficulty", s.ChallengeDifficulty) + lg.Debug("difficulty check failed", "response", response, "difficulty", rule.Challenge.Difficulty) templ.Handler(web.Base("Oh noes!", web.ErrorPage("invalid response")), templ.WithStatus(http.StatusForbidden)).ServeHTTP(w, r) failedValidations.Inc() return diff --git a/lib/anubis_test.go b/lib/anubis_test.go index 58c8834..79a0532 100644 --- a/lib/anubis_test.go +++ b/lib/anubis_test.go @@ -34,6 +34,79 @@ func spawnAnubis(t *testing.T, opts Options) *Server { return s } +type challenge struct { + Challenge string `json:"challenge"` +} + +func makeChallenge(t *testing.T, ts *httptest.Server) challenge { + t.Helper() + + resp, err := ts.Client().Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil) + if err != nil { + t.Fatalf("can't request challenge: %v", err) + } + defer resp.Body.Close() + + var chall challenge + if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil { + t.Fatalf("can't read challenge response body: %v", err) + } + + return chall +} + +// Regression test for CVE-2025-24369 +func TestCVE2025_24369(t *testing.T) { + pol := loadPolicies(t, "") + pol.DefaultDifficulty = 4 + + srv := spawnAnubis(t, Options{ + Next: http.NewServeMux(), + Policy: pol, + + CookieDomain: "local.cetacean.club", + CookiePartitioned: true, + CookieName: t.Name(), + }) + + ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv)) + defer ts.Close() + + chall := makeChallenge(t, ts) + calcString := fmt.Sprintf("%s%d", chall.Challenge, 0) + calculated := internal.SHA256sum(calcString) + nonce := 0 + elapsedTime := 420 + redir := "/" + + cli := ts.Client() + cli.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil) + if err != nil { + t.Fatalf("can't make request: %v", err) + } + + q := req.URL.Query() + q.Set("response", calculated) + q.Set("nonce", fmt.Sprint(nonce)) + q.Set("redir", redir) + q.Set("elapsedTime", fmt.Sprint(elapsedTime)) + req.URL.RawQuery = q.Encode() + + resp, err := cli.Do(req) + if err != nil { + t.Fatalf("can't do challenge passing") + } + + if resp.StatusCode == http.StatusFound { + t.Log("Regression on CVE-2025-24369") + t.Errorf("wanted HTTP status %d, got: %d", http.StatusForbidden, resp.StatusCode) + } +} + func TestCookieSettings(t *testing.T) { pol := loadPolicies(t, "") pol.DefaultDifficulty = 0 @@ -72,8 +145,9 @@ func TestCookieSettings(t *testing.T) { nonce := 0 elapsedTime := 420 redir := "/" + calculated := "" calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce) - calculated := internal.SHA256sum(calcString) + calculated = internal.SHA256sum(calcString) req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil) if err != nil {