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.
This commit is contained in:
Xe Iaso
2025-08-20 16:02:15 +00:00
parent e8dfff6350
commit 7b63d09e52
2 changed files with 13 additions and 0 deletions

View File

@@ -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)

View File

@@ -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?
}