From 7b63d09e52103874cfbb4350594cf7a404bbbdad Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Wed, 20 Aug 2025 16:02:15 +0000 Subject: [PATCH] fix(lib): ensure issued challenges don't get double-spent Closes #1002 TL;DR: challenge IDs were not validated at time of token issuance. A dedicated attacker could solve a challenge once and reuse it across multiple sessons in order to mint additional tokens. With the advent of store based challenge issuance in #749, this means that these challenge IDs are only good for 30 minutes. Websites using the most recent version of Anubis have limited exposure to this problem. Websites using older versions of Anubis have a much more increased exposure to this problem and are encouraged to keep this software updated as often and as frequently as possible. --- lib/anubis.go | 12 ++++++++++++ lib/challenge/challenge.go | 1 + 2 files changed, 13 insertions(+) diff --git a/lib/anubis.go b/lib/anubis.go index af7238d8..3fd9e68f 100644 --- a/lib/anubis.go +++ b/lib/anubis.go @@ -454,6 +454,12 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { return } + if chall.Spent { + lg.Error("double spend prevented", "reason", "double_spend") + s.respondWithError(w, r, fmt.Sprintf("%s: %s", localizer.T("internal_server_error"), "double_spend")) + return + } + impl, ok := challenge.Get(chall.Method) if !ok { lg.Error("check failed", "err", err) @@ -527,6 +533,12 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) { s.SetCookie(w, CookieOpts{Path: cookiePath, Host: r.Host, Value: tokenString}) + chall.Spent = true + j := store.JSON[challenge.Challenge]{Underlying: s.store} + if err := j.Set(r.Context(), "challenge:"+chall.ID, *chall, 30*time.Minute); err != nil { + lg.Debug("can't update information about challenge", "err", err) + } + challengesValidated.WithLabelValues(rule.Challenge.Algorithm).Inc() lg.Debug("challenge passed, redirecting to app") http.Redirect(w, r, redir, http.StatusFound) diff --git a/lib/challenge/challenge.go b/lib/challenge/challenge.go index 1200e330..2553d0cd 100644 --- a/lib/challenge/challenge.go +++ b/lib/challenge/challenge.go @@ -9,4 +9,5 @@ type Challenge struct { RandomData string `json:"randomData"` // The random data the client processes IssuedAt time.Time `json:"issuedAt"` // When the challenge was issued Metadata map[string]string `json:"metadata"` // Challenge metadata such as IP address and user agent + Spent bool `json:"spent"` // Has the challenge already been solved? }