{"id":"GHSA-g7m4-839x-ch6v","summary":"spomky-labs/otphp: Unbounded digits parameter in a provisioning URI triggers an uncaught DivisionByZeroError in OTP generation","details":"## Summary\n\nThe `digits` parameter parsed from a provisioning URI is validated only with a lower bound (`$value \u003e 0`) and has no upper bound (`src/OTP.php:353-357`). OTP generation computes `$code % (10 ** $this-\u003egetDigits())` (`src/OTP.php:283`). When `digits` is large enough that `10 ** digits` overflows PHP's integer range and the `(int)` cast yields `0` (around `digits \u003e= 40` on 64-bit PHP 8.x), the modulo operand becomes `0` and PHP raises a `DivisionByZeroError`.\n\n## Impact\n\n`OTPHP\\Factory::loadFromProvisioningUri()` forwards the attacker-controlled `digits` query value to `setParameter('digits', $value)`, so a hostile URI such as `otpauth://totp/Alice?secret=JBSWY3DPEHPK3PXP&digits=50` produces an OTP object whose `at()`, `now()`, and `verify()` all throw `DivisionByZeroError`. Because `DivisionByZeroError` extends `Error` (not `Exception`), callers that guard OTP generation with a `catch (\\Exception)` do not catch it, turning a malformed URI into an unhandled fatal error (denial of service of the verification path).\n\nMeasured threshold on PHP 8.3: `digits = 30` works, `digits \u003e= 40` throws `DivisionByZeroError: Modulo by zero`.\n\n## Affected component\n\n- `src/OTP.php:353-357` — `digits` parameter callback (no upper bound)\n- `src/OTP.php:283` — `$code % (10 ** $this-\u003egetDigits())`\n\n## Proof of concept\n\n```php\nuse OTPHP\\Factory;\nuse OTPHP\\InternalClock;\n\n$otp = Factory::loadFromProvisioningUri(\n    'otpauth://totp/Alice?secret=JBSWY3DPEHPK3PXP&digits=50',\n    new InternalClock()\n);\n$otp-\u003eat(0); // DivisionByZeroError: Modulo by zero (escapes catch (\\Exception))\n```\n\n## Remediation\n\nEnforce a sane upper bound on `digits` in the parameter validation callback (e.g. reject values above 8–10, the practical range for OTPs) so that an out-of-range value is rejected with a documented exception instead of producing an object that fails later with an uncatchable `Error`.","modified":"2026-09-10T03:50:49.441248787Z","published":"2026-06-18T20:45:47Z","database_specific":{"nvd_published_at":null,"cwe_ids":["CWE-1284","CWE-369"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-06-18T20:45:47Z"},"references":[{"type":"WEB","url":"https://github.com/Spomky-Labs/otphp/security/advisories/GHSA-g7m4-839x-ch6v"},{"type":"WEB","url":"https://github.com/FriendsOfPHP/security-advisories/blob/master/spomky-labs/otphp/GHSA-g7m4-839x-ch6v.yaml"},{"type":"PACKAGE","url":"https://github.com/Spomky-Labs/otphp"}],"affected":[{"package":{"name":"spomky-labs/otphp","ecosystem":"Packagist","purl":"pkg:composer/spomky-labs/otphp"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"11.4.3"}]}],"versions":["1.0.1-stable","11.0.3","11.1.0","11.1.1","11.2.0","11.2.1","11.2.2","11.3.0","11.4.0","11.4.1","11.4.2","5.0.0","v1.0.0-stable","v10.0.0","v10.0.1","v10.0.2","v10.0.3","v11.0.0","v11.0.1","v11.0.2","v2.0.0-stable","v2.0.1-stable","v2.0.2-stable","v3.0.0-stable","v3.0.1-stable","v3.1.0","v3.1.1","v4.0.0","v4.0.1","v4.0.2","v4.0.3","v4.0.4","v5.0.1","v6.0.0","v6.0.1","v6.0.2","v6.0.3","v6.0.4","v7.0.0","v7.0.1","v7.0.2","v7.0.3","v7.0.4","v8.0.0","v8.1.0","v8.2.0","v8.3.0","v8.3.1","v8.3.2","v8.3.3","v9.0.0","v9.0.0-alpha1","v9.0.1","v9.0.2","v9.0.3","v9.1.0","v9.1.1","v9.1.2","v9.1.3","v9.1.4"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-g7m4-839x-ch6v/GHSA-g7m4-839x-ch6v.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"}]}