knadh/listmonk · warning
captcha verification failed
Error message
captcha verification failed
What it means
verifyAltcha returns this when altcha.VerifySolution successfully evaluates the payload but reports the proof-of-work solution is invalid — the client did not solve the challenge correctly, or the payload was signed with a different HMAC key than the server now holds. It is the expected rejection for non-solved or tampered Altcha payloads.
Source
Thrown at internal/captcha/captcha.go:194
return err, true
}
if !r.Success {
return fmt.Errorf("hCaptcha failed: %s", strings.Join(r.ErrorCodes, ",")), false
}
return nil, true
}
// verifyAltcha verifies an Altcha response.
func (c *Captcha) verifyAltcha(payload string) (error, bool) {
valid, err := altcha.VerifySolution(payload, c.altcha.HMACKey, true)
if err != nil {
return fmt.Errorf("failed to verify captcha solution: %w", err), false
}
if !valid {
return fmt.Errorf("captcha verification failed"), false
}
// Disallow token reuse.
if _, err := tmptokens.Check(payload); err == nil {
return fmt.Errorf("captcha token already used"), false
}
tmptokens.Set(payload, 5*time.Minute, nil)
return nil, true
}
View on GitHub (pinned to 670c01717d)
Solutions
- Treat it as a client error: return 400/re-serve the form with a fresh challenge and let the user retry.
- Confirm the HMAC key hasn't changed since challenges were issued; if rotated, old in-flight challenges fail — restart form flows after rotation.
- Log the payload's challenge/salt fields (not the payload itself) to distinguish tampering from key mismatch.
- Ensure the client widget actually completed the proof-of-work before submission (check altcha 'verified' state).
Example fix
// before: key rotated in config without invalidating sessions
// after: version the key and accept only current-generation challenges
const keyVersion = 2
if challengeKeyVersion(payload) != keyVersion {
return errors.New("stale captcha challenge, reload the page"), false
} Defensive patterns
Strategy: fallback
Validate before calling
if err, ok := captcha.Verify(payload); !ok {
// re-issue a fresh challenge so the user can retry with new proof-of-work
challenge, err := captcha.GenerateChallenge(r.Context())
if err != nil {
http.Error(w, "captcha unavailable", http.StatusServiceUnavailable)
return
}
renderFormWithChallenge(w, challenge)
return
} Try / catch
if err, ok := captcha.Verify(payload); !ok {
if err != nil && err.Error() == "captcha verification failed" {
// solution invalid: serve a new challenge and ask the user to retry
http.Error(w, "captcha failed — reload and try again", http.StatusBadRequest)
return
}
} Prevention
- Always re-issue a fresh challenge after an invalid solution instead of reusing the old one
- Announce and coordinate HMAC key rotations — old challenges fail after rotation
- Rate-limit verification attempts to blunt proof-of-work-bypass bots
- Ensure the client widget reports 'verified' state before enabling the submit button
When it happens
Trigger: Verify is called with a payload whose computed hash does not meet the challenge target, whose salt/algorithm fields were modified, or which was generated under an old HMAC key after a server-side key rotation.
Common situations: Bots submitting random payloads without solving proof-of-work (the dominant case); users bypassing the widget and posting raw form data; HMAC key changed on the server while clients still hold challenges from before the restart; complexity raised so slow clients time out and submit partial solutions.
Related errors
- failed to create Altcha challenge: %w
- failed to verify captcha solution: %w
- failed to marshal Altcha challenge: %w
- hCaptcha failed: %s
- no captcha provider enabled
AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01).
Data as JSON: /api/errors/9f35923269a0c631.
Report an issue: GitHub.