v1.23.0
Security fix. A proof of work is now a precondition for a token rather than one more scored signal.
Fixed
A request with no proof of work was issued a valid token. curl -X POST /api/verify -d '{"siteKey":"x","signals":{}}' — no browser, no PoW, empty signals — minted tokens on every server, and they passed siteverify. Present in v1.21.0 and earlier.
The detectors were not at fault; they fired. The final score is a weighted sum, so a category contributes at most its own weight: bot is weighted 0.13, and every PoW failure firing at once reached 0.1298 against a 0.5 threshold. A forged solution referencing a challenge that was never issued produced seven detections and still scored 0.4003.
- A valid proof of work is now checked outside the score, alongside the hostname allowlist:
success = score < 0.5 && hostnameAllowed && powSatisfied. - Missing, unverifiable and nonce-unbound solutions are marked dispositive, so the reported score is 0.9 rather than 0.4.
- Refusals name the failed precondition in a new
reasonfield:pow_not_satisfiedorhostname_not_allowed.
Impact on legitimate traffic: none measured
The bundled widget already solves a challenge on every path and aborts rather than submit without one. Measured on the benchmark harness, which completes the real handshake: human false-positive rate 0.00% (0/126), human median score unchanged at 0.097, agent TPR 97.33%.
Breaking
A custom client calling /api/verify directly must now complete the handshake: GET /api/pow/challenge, echo the nonce in signals.meta.challengeNonce, commit the signals into the PoW input, and send powSolution. There is no flag to restore the old behaviour.
See Upgrading to 1.23.0.