knadh/listmonk · error

failed to verify captcha solution: %w

Error message

failed to verify captcha solution: %w

What it means

verifyAltcha wraps an error returned by altcha.VerifySolution when the submitted Altcha payload cannot even be processed — malformed base64/JSON payload, signature check error, or other altcha internal failure. This differs from a mere incorrect solution (which returns 'captcha verification failed'); here the payload itself could not be evaluated.

Source

Thrown at internal/captcha/captcha.go:190

	}

	var r hCaptchaResp
	if err := json.Unmarshal(body, &r); err != nil {
		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

  1. Unwrap the error to see whether it is a base64/JSON parse error or a signature error.
  2. Ensure server and client use the same altcha algorithm version and HMAC key.
  3. Reject with 400 immediately — malformed payloads indicate bots or broken clients, not retryable server issues.
  4. After rotating the HMAC key, invalidate old challenges (they fail signature checks).

Example fix

// before: same handling for parse errors and wrong solutions
valid, err := altcha.VerifySolution(payload, c.altcha.HMACKey, true)
// after: distinguish client error from verification failure
if _, ok := err.(*base64.CorruptInputError); ok {
  return errors.New("malformed captcha payload"), false // 400
}
Defensive patterns

Strategy: validation

Validate before calling

payload := r.PostFormValue("altcha")
if payload == "" {
  return errors.New("altcha payload missing")
}
decoded, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
  return errors.New("altcha payload is not valid base64")
}
var p map[string]any
if err := json.Unmarshal(decoded, &p); err != nil {
  return errors.New("altcha payload is not valid JSON")
}

Type guard

func isWellFormedAltchaPayload(payload string) bool {
  decoded, err := base64.StdEncoding.DecodeString(payload)
  if err != nil {
    return false
  }
  var probe struct {
    Algorithm string `json:"algorithm"`
    Challenge string `json:"challenge"`
    Salt      string `json:"salt"`
    Signature string `json:"signature"`
  }
  return json.Unmarshal(decoded, &probe) == nil && probe.Challenge != "" && probe.Signature != ""
}

Try / catch

if err, ok := captcha.Verify(payload); !ok {
  if err != nil && strings.Contains(err.Error(), "failed to verify captcha solution") {
    // unparseable payload: client error, not retryable server-side
    log.Warn("malformed altcha payload (possible bot or version mismatch)", "err", err)
    http.Error(w, "invalid captcha payload", http.StatusBadRequest)
    return
  }
}

Prevention

When it happens

Trigger: Calling Verify with ProviderAltcha where the payload string is not a valid base64-encoded signed challenge response — truncated payloads, tampered payloads, payloads generated against a different HMAC key, or non-base64 garbage submitted by clients.

Common situations: Client JS version producing a payload format the server's altcha library can't parse (version mismatch); attacker probing the endpoint with junk payloads; HMAC key rotated server-side while old pages still hold challenges signed with the previous key.

Related errors


AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01). Data as JSON: /api/errors/aadffb021e79cfc1. Report an issue: GitHub.