aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--VERSION2
-rw-r--r--docs/docs/CHANGELOG.md13
-rw-r--r--lib/anubis.go19
-rw-r--r--lib/anubis_test.go76
4 files changed, 98 insertions, 12 deletions
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 {