{"record":{"id":"b9f09c40102fd815","repo":"ellite/Wallos","slug":"invalid-counter-parameter","errorCode":null,"errorMessage":"Invalid \"counter\" parameter.","messagePattern":"Invalid \"counter\" parameter\\.","errorType":"exception","errorClass":"InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"libs/OTPHP/HOTP.php","lineNumber":55,"sourceCode":"        $htop->setCounter(self::DEFAULT_COUNTER);\n        $htop->setDigest(self::DEFAULT_DIGEST);\n        $htop->setDigits(self::DEFAULT_DIGITS);\n\n        return $htop;\n    }\n\n    public static function generate(): self\n    {\n        return self::createFromSecret(self::generateSecret());\n    }\n\n    /**\n     * @return 0|positive-int\n     */\n    public function getCounter(): int\n    {\n        $value = $this->getParameter('counter');\n        (is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid \"counter\" parameter.');\n\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    {","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/ellite/Wallos/blob/52820e87ca5a6e105fdbb7f1c0c681bc0cfee2fd/libs/OTPHP/HOTP.php#L37-L73","documentation":"HOTP::getCounter reads the 'counter' parameter and throws InvalidArgumentException unless it is an int >= 0. The library enforces that the stored HOTP counter is a non-negative integer before use in URI generation or verification.","triggerScenarios":"Calling getCounter() (directly or via getProvisioningUri()/verify()) when the 'counter' parameter was never set, was set to a non-int, or was stored as a numeric string from parsing a provisioning URI query parameter.","commonSituations":"Creating an HOTP object without calling setParameter('counter', ...), loading a provisioning URI lacking a counter= query parameter, or counter arriving as string from JSON/DB storage.","solutions":["Set a valid counter before use: $hotp->setParameter('counter', 0); or pass counter in HOTP::create().","Cast values when loading from external storage: (int) $row['counter'] before storing.","When parsing a provisioning URI, ensure it includes counter=<int> in the query string.","Guard with is_int($counter) && $counter >= 0 before calling verify()/getProvisioningUri()."],"exampleFix":"// before\n$hotp = HOTP::createFromSecret($secret); // counter never set\n$uri = $hotp->getProvisioningUri(); // throws\n// after\n$hotp = HOTP::create($secret, 0, 'sha1', 6); // counter provided\n$uri = $hotp->getProvisioningUri();","handlingStrategy":"validation","validationCode":"$counter = $hotp->getParameter('counter');\nif (!is_int($counter) || $counter < 0) {\n    $hotp->setParameter('counter', 0);\n}","typeGuard":"function hasValidCounter(object $hotp): bool {\n    $c = $hotp->getParameter('counter');\n    return is_int($c) && $c >= 0;\n}","tryCatchPattern":"try {\n    $uri = $hotp->getProvisioningUri();\n} catch (InvalidArgumentException $e) {\n    $hotp->setParameter('counter', 0);\n    $uri = $hotp->getProvisioningUri();\n}","preventionTips":["Always initialize the counter when constructing HOTP objects","Cast counters from DB/JSON to int at the storage boundary","Include counter= in any provisioning URI you generate for HOTP","Add an integration test that round-trips HOTP provisioning URIs"],"tags":["php","otp","hotp","validation"],"backgroundTag":"invalid-argument-value","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"}