CVE-2026-45610: WWBN AVideo plugin/LoginControl/set.json.php: 2FA toggle endpoint has no CSRF protection, letting an attacker page silently disable a logged-in victim's 2FA

Published May 15, 2026
·
Updated

Summary

Type: Cross-site request forgery on the 2FA toggle. plugin/LoginControl/set.json.php accepts POST type=set2FA value=false, calls LoginControl::setUser2FA(User::getId(), false) on the session-authenticated user, and returns. There is no forbidIfIsUntrustedRequest() call, no isTokenValid() check, no X-CSRF-Token/SameSite enforcement, and no re-authentication step. A cross-origin page that the victim visits while logged into the AVideo dashboard issues the POST via a hidden form (or fetch without credentials:"omit") and disables the victim's 2FA in one request. The next phishing/credential-stuffing attempt against that account no longer needs the second factor. File: plugin/LoginControl/set.json.php, lines 1-37. Root cause: the developer relied on the User::isLogged() check at line 9 as the only auth, then dispatched directly into LoginControl::setUser2FA(User::getId(), $value=='true'). Other AVideo state-changing endpoints in the same codebase (videoUpdateUsage.json.php, videoStatus.json.php, videoRotate.json.php, etc.) call forbidIfIsUntrustedRequest('<name>') to compare Origin/Referer against the AVideo domain; this endpoint simply omits the call. The session cookie carries the user's identity on every cross-origin POST, so any attacker page can speak for the logged-in user on this endpoint.

Affected Code

File: plugin/LoginControl/set.json.php, lines 1-37.

php <?php requireonce '../../videos/configuration.php'; sessionwriteclose(); header('Content-Type: application/json');

$obj = new stdClass(); $obj->error = true; $obj->msg = ""; if (!User::isLogged()) { $obj->msg = "Not logged"; die(jsonencode($obj)); } if (empty($POST['type'])) { $obj->msg = "Type is empty"; die(jsonencode($obj)); } if (!isset($POST['value'])) { $obj->msg = "value is empty"; die(jsonencode($obj)); }

$cu = AVideoPlugin::loadPluginIfEnabled('LoginControl');

if (empty($cu)) { $obj->msg = "Plugin not enabled"; die(jsonencode($obj)); }

$obj->error = false; switch ($POST['type']) { case 'set2FA': LoginControl::setUser2FA(User::getId(), $POST['value']=="true" ? true : false); // <-- BUG: no CSRF gate, no re-auth break; }

die(jsonencode($obj));

Why it's wrong: disabling a victim's second factor is exactly the kind of state change the AVideo CSRF helper forbidIfIsUntrustedRequest() exists to protect. Compare with objects/commentslike.json.php:18 (forbidIfIsUntrustedRequest('commentslike')) — comments-likes get CSRF protection, but the 2FA toggle does not. Beyond CSRF, security-sensitive toggles like 2FA-disable conventionally also require either the current 2FA code or a password re-prompt: a malicious browser extension, an XSS that lands in any AVideo subdomain, or a compromised tab can otherwise flip the bit silently. None of those mitigations exist here.

Exploit Chain

1. Attacker hosts https://attacker.example/avideo-2fa-off.html containing: html <form id="f" action="https://avideo.example/plugin/LoginControl/set.json.php" method="POST"> <input type="hidden" name="type" value="set2FA"> <input type="hidden" name="value" value="false"> </form> <script>document.getElementById('f').submit();</script> State: page is live and indexable. 2. Attacker delivers the page to a victim who is logged in to avideo.example (open redirect on a trusted partner, ad campaign, IM phishing link, encyclopedic-looking forum post). The victim's browser opens the page; the form auto-submits to AVideo. State: cross-origin POST hits set.json.php with the victim's session cookie attached (the cookie's SameSite attribute is set to Lax/None by AVideo's defaults so the cross-origin POST succeeds for top-level navigations). 3. set.json.php:9 confirms User::isLogged() (true, victim's session is valid). Lines 13-19 see type=set2FA, value=false. Line 30-32 calls LoginControl::setUser2FA(victimuserid, false) and persists the change. State: victim's 2FA is now disabled in users.externalOptions.LoginControl.is2FAEnabled. 4. Victim sees a generic "operation completed" JSON response in a redirected browser tab (or no visible feedback at all if the form lands in an iframe). State: victim notices nothing unusual. 5. Attacker (in a separate session) attempts credential stuffing or password-spray against avideo.example/objects/login.json.php. Without the second factor, any one of: a previously leaked password, a successful credential-stuffing match, or a spear-phishing-collected password completes the login. State: attacker holds full session for victim's account. 6. Final state: the second factor that the victim explicitly enabled was silently disabled across the wire by visiting an attacker-hosted page. The whole chain takes one HTTP POST and zero clicks beyond the initial visit.

Security Impact

Severity: sec-moderate. CVSS 6.5: network attack, low complexity, low privileges (the attacker themselves are unauthenticated; the victim must be a logged-in AVideo user; this is captured by PR:L because the action's effect requires the victim's session), user interaction required (visit attacker page), scope unchanged, no confidentiality directly, high integrity (the victim's 2FA configuration is silently corrupted), no availability claim. Attacker capability: with one cross-origin POST, the attacker turns a victim's 2FA-protected account into a plain password-only account. Combined with any password leak, credential-stuffing match, or successful phishing of the password, the account is fully compromised. The change is permanent until the victim notices and re-enables 2FA, and AVideo does not raise an audit-log event when 2FA is disabled (see LoginControl::setUser2FA — it simply writes the boolean), so detection is unlikely. Preconditions: AVideo deployment with the LoginControl plugin enabled (the plugin shipping the 2FA feature); the victim is logged in to AVideo at the moment they visit the attacker page; the AVideo session cookie does not have SameSite=Strict (the deployment default is SameSite=Lax per objects/phpsessionid.json.php:53, which still allows cross-origin top-level POSTs from a form auto-submit). Differential: source-inspection-verified. set.json.php does not contain forbidIfIsUntrustedRequest, isTokenValid, verifyToken, or any equivalent string; the entire body of the file is reproduced above. With the suggested fix below, the same cross-origin POST returns a 403 with Invalid Request and the setUser2FA call never fires.

Suggested Fix

Add the same CSRF gate every other state-changing endpoint in this codebase uses, and require the current 2FA code (or a password re-prompt) when the user is disabling the second factor.

diff --- a/plugin/LoginControl/set.json.php +++ b/plugin/LoginControl/set.json.php @@ -9,6 +9,8 @@ if (!User::isLogged()) { $obj->msg = "Not logged"; die(jsonencode($obj)); } +forbidIfIsUntrustedRequest('LoginControl-set'); + if (empty($POST['type'])) { $obj->msg = "Type is empty"; die(jsonencode($obj)); @@ -28,7 +30,15 @@ $obj->error = false; switch ($POST['type']) { case 'set2FA': - LoginControl::setUser2FA(User::getId(), $POST['value']=="true" ? true : false); + $newValue = ($POST['value'] == 'true'); + // Require the current 2FA code (or a password re-prompt) when DISABLING 2FA; + // turning it on is fine, turning it off needs a step-up. + if (!$newValue && !LoginControl::confirmStepUpForCurrentUser($POST['confirm'] ?? '')) { + $obj->error = true; + $obj->msg = ('Re-authentication required to disable 2FA'); + die(jsonencode($obj)); + } + LoginControl::setUser2FA(User::getId(), $newValue); break; }

Defence-in-depth: the AVideo session cookie should be issued with SameSite=Strict for the management dashboard's first-party POSTs; the public read-only player can keep a separate SameSite=Lax cookie. Audit-log every 2FA-disable event with the source IP and user agent so an unexpected disable is visible to the operator.

Other sources

WWBN AVideo is an open source video platform. In 29.0 and earlier, there is a cross-site request forgery vulnerability on the 2FA toggle. plugin/LoginControl/set.json.php accepts POST type=set2FA value=false, calls LoginControl::setUser2FA(User::getId(), false) on the session-authenticated user, and returns. There is no forbidIfIsUntrustedRequest() call, no isTokenValid() check, no X-CSRF-Token/SameSite enforcement, and no re-authentication step. A cross-origin page that the victim visits while logged into the AVideo dashboard issues the POST via a hidden form (or fetch without credentials:"omit") and disables the victim's 2FA in one request.

MITRE

Affected Software

2 affected components
composer/WWBN/AVideo<=29.0
WWBN AVideo<=29.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Configuration

    In plugin/LoginControl/set.json.php, for the 2FA toggle endpoint (POST type=set2FA), add the CSRF protection call `forbidIfIsUntrustedRequest('LoginControl-set')` so cross-origin untrusted requests are rejected (e.g., return 403 with `Invalid Request` as described).

    plugin/LoginControl/set.json.php CSRF gate = Add forbidIfIsUntrustedRequest('LoginControl-set')
  2. Configuration

    In plugin/LoginControl/set.json.php, when handling `case 'set2FA'` (POST type=set2FA), implement step-up re-authentication only for disabling 2FA: after parsing `$_POST['value']` into `$newValue`, call `LoginControl::confirmStepUpForCurrentUser($_POST['confirm'] ?? ...)` (or equivalent) when `$newValue` is false; only then call `LoginControl::setUser2FA(User::getId(), $newValue)`. The provided diff indicates turning ON is fine, but turning OFF requires step-up (current 2FA code or password re-prompt).

    plugin/LoginControl/set.json.php 2FA disable step-up = Require current 2FA code or password re-prompt only when disabling
  3. Compensating control

    Issue AVideo management/dashboard session cookies with `SameSite=Strict` for first-party POSTs, while keeping the public read-only player cookie at `SameSite=Lax`. This reduces the ability for cross-origin top-level POSTs to carry the victim session to plugin/LoginControl/set.json.php.

Event History

May 15, 2026
Advisory Published
via GitHub·06:34 PM
Data Sourced
via GitHub·06:34 PM
DescriptionSeverityWeaknessAffected Software
May 29, 2026
CVE Published
via MITRE·01:13 PM
Data Sourced
via MITRE·01:13 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·02:16 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What is the severity of CVE-2026-45610?

CVE-2026-45610 has been classified with a high severity due to its nature of enabling cross-site request forgery (CSRF) on the 2FA toggle.

2

How do I fix CVE-2026-45610?

To fix CVE-2026-45610, implement a method to verify the authenticity of requests before toggling 2FA settings.

3

Which software versions are affected by CVE-2026-45610?

CVE-2026-45610 affects versions of WWBN/AVideo up to and including 29.0.

4

What kind of attack is possible with CVE-2026-45610?

CVE-2026-45610 allows attackers to exploit CSRF vulnerabilities to disable two-factor authentication for authenticated users.

5

How can I mitigate risks associated with CVE-2026-45610?

Mitigation for CVE-2026-45610 includes implementing CSRF tokens and improving the validation of incoming requests to the affected endpoint.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203