{"record":{"id":"b0f4ee90a6ae63cd","repo":"knadh/listmonk","slug":"captcha-token-already-used","errorCode":null,"errorMessage":"captcha token already used","messagePattern":"captcha token already used","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/captcha/captcha.go","lineNumber":199,"sourceCode":"\t}\n\n\treturn nil, true\n}\n\n// verifyAltcha verifies an Altcha response.\nfunc (c *Captcha) verifyAltcha(payload string) (error, bool) {\n\tvalid, err := altcha.VerifySolution(payload, c.altcha.HMACKey, true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to verify captcha solution: %w\", err), false\n\t}\n\n\tif !valid {\n\t\treturn fmt.Errorf(\"captcha verification failed\"), false\n\t}\n\n\t// Disallow token reuse.\n\tif _, err := tmptokens.Check(payload); err == nil {\n\t\treturn fmt.Errorf(\"captcha token already used\"), false\n\t}\n\ttmptokens.Set(payload, 5*time.Minute, nil)\n\n\treturn nil, true\n}\n","sourceCodeStart":181,"sourceCodeEnd":205,"githubUrl":"https://github.com/knadh/listmonk/blob/670c01717d48647093335cc23a6be6f4b79c3b6b/internal/captcha/captcha.go#L181-L205","documentation":"This error is returned by verifyAltcha when a client submits an ALTCHA proof-of-work captcha token that has already been verified. The manager keeps a short-lived store (tmptokens) of previously used tokens; if Check() finds the token present, it means the exact challenge solution was already consumed and verified, so verification is refused to prevent replay attacks.","triggerScenarios":"A Verify call is made with a payload/token that was already successfully verified within the previous 5 minutes (the token's stored TTL). This happens on double-submits, form resubmissions, browser back-button resends, or a client retrying a request that actually succeeded the first time.","commonSituations":"Users double-clicking a submit button, frontend code retrying a failed HTTP request (timeouts) where the first request actually succeeded, automated tests reusing a captured token across multiple verification calls, or proxy/load-balancer request duplication.","solutions":["Generate a fresh ALTCHA challenge and solve it for each submission instead of reusing a captured token","Make the client-side submit handler idempotent: disable the button and do not resend on retry; re-solve the captcha for the retry","Shorten frontend retry logic or re-request a challenge on 4xx responses before retrying","If tokens expire too aggressively in tests, mint a new token per test case rather than sharing one"],"exampleFix":"// before\nawait verify(oldPayload) // second call: 'captcha token already used'\n// after\nconst challenge = await fetchNewChallenge()\nconst payload = await solveAltcha(challenge) // fresh solve per submission\nawait verify(payload)","handlingStrategy":"validation","validationCode":"// Client-side: always solve a fresh challenge per submission\nconst challenge = await fetch('/api/captcha/challenge').then(r => r.json())\nconst payload = await solveAltcha(challenge) // never cache/reuse across requests","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Solve a new captcha challenge for every submission, including retries","Disable the submit button after first click to prevent double-submits","On network retry, treat the prior request as possibly-succeeded and re-solve the captcha rather than replaying the token","Never share a verified token across multiple requests or test cases"],"tags":["captcha","altcha","replay","token-reuse"],"backgroundTag":"captcha-token-reuse","analyzedSha":"670c01717d48647093335cc23a6be6f4b79c3b6b","analyzedAt":"2026-09-01T03:39:35.452Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}