
Cloud Vulnerability DB
A community-led vulnerabilities database
The counter parameter parsed from a provisioning URI is validated only with a lower bound ($value >= 0) and has no upper bound (src/HOTP.php:120-129). HOTP::verify() checks the code against every counter from $counter to $counter + $window (src/HOTP.php:155-157). When $counter + $window reaches PHP_INT_MAX and no counter matches, ++$i takes $i past PHP_INT_MAX and PHP turns it into a float. The loop condition, now evaluated in floating point, still holds, so the float is passed to the int-typed OTP::at() and PHP raises a TypeError. A correct code at counter PHP_INT_MAX fails the same way, because updateCounter($i + 1) receives a float.
OTPHP\Factory::loadFromProvisioningUri() forwards the attacker-controlled counter query value to setParameter('counter', $value), which casts it to int, so larger decimal values are also loaded as PHP_INT_MAX. A hostile URI such as otpauth://hotp/Alice?secret=JBSWY3DPEHPK3PXP&counter=9223372036854775807 therefore produces an HOTP object on which every verify() call throws a TypeError, whether the code is wrong or correct and whether a window is passed or not. Because TypeError extends Error (not Exception) and does not implement OTPHP\Exception\OTPExceptionInterface, callers that guard verification with catch (\Exception) or catch (OTPExceptionInterface) do not catch it, turning a malformed URI into an unhandled fatal error (denial of service of the verification path).
A smaller counter fails the same way as soon as counter + window >= PHP_INT_MAX. Measured on PHP 8.2 and 8.5 with 11.4.3 and 11.5.0, for a wrong code and a window of 10: counter = PHP_INT_MAX - 11 returns false, counter = PHP_INT_MAX - 10 throws TypeError.
Releases older than 9.0 fail on the same input as well, by exhausting memory instead of raising a TypeError.
Line numbers are those of 11.4.3 and 11.5.0.
src/HOTP.php:120-129 — counter parameter callback (no upper bound)src/HOTP.php:155-157 — window loop of verifyOtpWithWindow(): $i <= $counter + $window, then $this->at($i) and $this->updateCounter($i + 1)use OTPHP\Factory;
use OTPHP\InternalClock;
$otp = Factory::loadFromProvisioningUri(
'otpauth://hotp/Alice?secret=JBSWY3DPEHPK3PXP&counter=9223372036854775807',
new InternalClock()
);
$otp->verify('000000'); // TypeError: OTPHP\OTP::at(): Argument #1 ($input) must be of type int, float given
$otp->verify($otp->at(PHP_INT_MAX)); // TypeError: OTPHP\HOTP::updateCounter(): Argument #1 ($counter) must be of type int, float givenNeither error is caught by catch (\Exception).
Upgrade to 11.4.4 or 11.5.1. HOTP::verify() now bounds its window loop without overflow and returns false once the counter is exhausted: the last counter it can accept is PHP_INT_MAX - 1.
If you cannot upgrade yet, do not call verify() on an HOTP object built from an untrusted provisioning URI when $counter > PHP_INT_MAX - 1 - $window, where $counter is the counter you pass to verify() ($otp->getCounter() if you pass none) and $window is the window you pass (0 if none).
The same releases also reject, with InvalidParameterException, three values that only the application can pass: a window of PHP_INT_MIN and a negative input to at(), both outside the documented types, and a TOTP timestamp so close to PHP_INT_MAX that adding the leeway overflows. None of them can come from a provisioning URI.
Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."