{"record":{"id":"080bee885237d1bd","repo":"ellite/Wallos","slug":"the-counter-must-be-at-least-0","errorCode":null,"errorMessage":"The counter must be at least 0.","messagePattern":"The counter must be at least 0\\.","errorType":"exception","errorClass":"InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"libs/OTPHP/HOTP.php","lineNumber":74,"sourceCode":"\n        return $value;\n    }\n\n    public function getProvisioningUri(): string\n    {\n        return $this->generateURI('hotp', [\n            'counter' => $this->getCounter(),\n        ]);\n    }\n\n    /**\n     * If the counter is not provided, the OTP is verified at the actual counter.\n     *\n     * @param null|0|positive-int $counter\n     */\n    public function verify(string $otp, null|int $counter = null, null|int $window = null): bool\n    {\n        $counter >= 0 || throw new InvalidArgumentException('The counter must be at least 0.');\n\n        if ($counter === null) {\n            $counter = $this->getCounter();\n        } elseif ($counter < $this->getCounter()) {\n            return false;\n        }\n\n        return $this->verifyOtpWithWindow($otp, $counter, $window);\n    }\n\n    public function setCounter(int $counter): void\n    {\n        $this->setParameter('counter', $counter);\n    }\n\n    /**\n     * @return array<non-empty-string, callable>\n     */","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/ellite/Wallos/blob/52820e87ca5a6e105fdbb7f1c0c681bc0cfee2fd/libs/OTPHP/HOTP.php#L56-L92","documentation":"HOTP::verify throws InvalidArgumentException when the optional $counter argument is a negative int. Since verify() accepts null|0|positive-int, any negative value is rejected outright before verification proceeds.","triggerScenarios":"Calling $hotp->verify($otp, -1) or passing a negative counter computed at runtime (e.g. $counter = $current - $window going below 0), or a negative value from user input/DB.","commonSituations":"Implementing look-ahead window verification where code computes counter minus window without clamping at 0; restoring state from a database with corrupt negative counters.","solutions":["Clamp the counter: $counter = max(0, $computedCounter); before calling verify().","Omit the $counter argument (pass null) to verify at the object's current counter.","Validate user-supplied counters: reject negatives at input boundary with a 4xx-style validation error.","Check stored counter values in your database for corruption/negative values."],"exampleFix":"// before\n$ok = $hotp->verify($otp, $storedCounter - $window);\n// after\n$ok = $hotp->verify($otp, max(0, $storedCounter - $window));","handlingStrategy":"validation","validationCode":"if ($counter !== null && (!is_int($counter) || $counter < 0)) {\n    throw new DomainException('Counter must be a non-negative integer');\n}","typeGuard":"function isValidCounter(mixed $counter): bool {\n    return $counter === null || (is_int($counter) && $counter >= 0);\n}","tryCatchPattern":"try {\n    $valid = $hotp->verify($otp, $counter);\n} catch (InvalidArgumentException $e) {\n    log_warning('Negative counter passed to verify', ['counter' => $counter]);\n    return false;\n}","preventionTips":["Clamp window arithmetic with max(0, ...) before calling verify","Validate user-supplied counters at the controller boundary","Treat counters as unsigned integers in your schema","Add unit tests for window-verification edge cases near 0"],"tags":["php","otp","hotp","argument-out-of-range"],"backgroundTag":"argument-out-of-range","analyzedSha":"52820e87ca5a6e105fdbb7f1c0c681bc0cfee2fd","analyzedAt":"2026-09-13T14:09:30.873Z","contentChangedAt":"2026-09-13T14:09:30.873Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}