-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The bug is the reject branch\n\nCVE-2026-43873 was assigned to WWBN AVideo, an open-source video-platform project, for a bug in `plugin/CloneSite/cloneClient.json.php`. The CloneSite plugin federates two AVideo instances. An admin on the local server triggers `plugin/CloneSite/cloneClient.json.php` to pull a fresh database dump and media set from a configured peer. The endpoint supports two invocation modes: a CLI mode for cron-driven backups (which passes the federation key as `argv[1]`) and a web mode that requires an authenticated admin session. Before April 24, 2026, the auth check at the top of the file read:\n\n```php\n$objClone = AVideoPlugin::getObjectDataIfEnabled(\"CloneSite\");\n\n$argv[1] = preg_replace(\"/[^A-Za-z0-9 ]/\", '', empty($argv[1])?'':$argv[1]);\n\nif (empty($objClone) || empty($argv[1]) || $objClone->myKey !== $argv[1]) {\n if (!User::isAdmin()) {\n $resp->msg = \"You can't do this\";\n $log->add(\"Clone: {$resp->msg}\");\n echo \"$objClone->myKey !== $argv[1]\";\n die(json_encode($resp));\n }\n}\n```\n\nPHP's web SAPI has no `$argv`. The `empty($argv[1])` check short-circuits true on every HTTP caller. The outer condition is true. The inner `User::isAdmin()` check is false for any caller without an authenticated admin session. The body of the inner condition runs. The `echo` runs. The `die(json_encode($resp))` runs.\n\nThe `Content-Type` header was set to `application/json` at the top of the file. The body on an unauthenticated request is the concatenation of the echo and the JSON:\n\n```\n4f8a1c2b3d4e5f6789abcdef01234567 !== {\"error\":true,\"msg\":\"You can't do this\"}\n```\n\nThe response is malformed JSON. `myKey` is the first token in it.\n\nThe patch is three lines added and one line removed:\n\n```diff\n-$argv[1] = preg_replace(\"/[^A-Za-z0-9 ]/\", '', empty($argv[1])?'':$argv[1]);\n+$cliKey = '';\n+if (isCommandLineInterface() && !empty($argv[1])) {\n+ $cliKey = preg_replace(\"/[^A-Za-z0-9 ]/\", '', $argv[1]);\n+}\n\n-if (empty($objClone) || empty($argv[1]) || $objClone->myKey !== $argv[1]) {\n+if (empty($objClone) || empty($cliKey) || !hash_equals((string) $objClone->myKey, (string) $cliKey)) {\n if (!User::isAdmin()) {\n $resp->msg = \"You can't do this\";\n $log->add(\"Clone: {$resp->msg}\");\n- echo \"$objClone->myKey !== $argv[1]\";\n die(json_encode($resp));\n }\n }\n```\n\nThree changes ride in the diff. The `isCommandLineInterface()` gate around the `argv[1]` read is defense-in-depth: it prevents a hypothetical caller setting `argv[1]` via query parameters under a non-default `register_argc_argv` configuration. The `hash_equals` upgrade is timing-safe comparison, which matters in some auth contexts and does not in this one, because the outer key-equality check has already failed before any timing channel could be observed. The third change is the deletion of the `echo`. The third change is the CVE. The other two are dressing.\n\n## What `$objClone->myKey` authorizes\n\nThe `myKey` is computed once per install. Its definition is in the CloneSite plugin's data object: `md5($global['systemRootPath'] . $global['salt'])`. Both inputs live in `videos/configuration.php` and are constant for the install's lifetime. The hash is the credential the local server presents to every CloneSite peer it federates with.\n\nThe presentation is in line 77 of the same file:\n\n```php\n$url = $objClone->cloneSiteURL\n . \"plugin/CloneSite/cloneServer.json.php?url=\" . urlencode($global['webSiteRootURL'])\n . \"&key={$objClone->myKey}\"\n . \"&useRsync=\" . intval($objClone->useRsync);\n```\n\nThe local client calls the peer's server endpoint, passing the local's own `webSiteRootURL` as the caller identity and the local's own `myKey` as the credential. The peer reads both off the query string in `cloneServer.json.php`:\n\n```php\n$resp->url = $_GET['url'];\n$resp->key = $_GET['key'];\n\n// ...\n\n$canClone = Clones::thisURLCanCloneMe($resp->url, $resp->key);\nif (empty($canClone->canClone)) {\n $resp->msg = $canClone->msg;\n die(json_encode($resp));\n}\n```\n\n`Clones::thisURLCanCloneMe` looks up the caller's URL in the peer's `clone_SitesAllowed` table and compares its stored `key` against the inbound `key`:\n\n```php\npublic static function thisURLCanCloneMe($url, $key) {\n // ...\n if ($clone->getKey() !== $key) {\n $resp->msg = \"Invalid Key\";\n return $resp;\n }\n if ($clone->getStatus() !== 'a') {\n $resp->msg = \"The URL {$url} is inactive in our Clone Server\";\n return $resp;\n }\n $resp->clone = $clone;\n $resp->canClone = true;\n return $resp;\n}\n```\n\nThe stored `key` on the peer's row is whatever the local sent the first time it called the peer. The local sends its own `myKey`. The peer's record of the local is keyed by the local's `myKey`. The leak from the local's reject branch is the credential the peer requires.\n\nWhen the check passes, the peer runs:\n\n```php\n$cmd = \"mysqldump -u {$mysqlUser} -p'{$mysqlPass}' --host {$mysqlHost} \"\n . \" --default-character-set=utf8mb4 {$mysqlDatabase} {$tablesList} > $sqlFile\";\nexec($cmd . \" 2>&1\", $output, $return_val);\n```\n\n`$sqlFile` is `{$clonesDir}{$resp->sqlFile}`, where `$clonesDir` is `videos/clones/` under the peer's webroot and `$resp->sqlFile` is `uniqid('Clone_mysqlDump_') . \".sql\"`. The dump lands in a directory the peer's webserver serves publicly. The filename returns to the caller as the `sqlFile` field of the JSON response. An attacker holding the leaked `myKey`, calling the peer's `cloneServer.json.php` with the local's `webSiteRootURL`, receives the filename of a fresh mysqldump and downloads it with a GET to `videos/clones/Clone_mysqlDump_.sql`.\n\nThe leak is on the local. The exfiltration is on the peer. The blast radius is every federation relationship the local maintains.\n\n## The commit message describes everything except the fix\n\nThe commit that closed the GHSA is titled *feat: Sanitize command line input for clone key validation*. The body is the URL of the advisory and nothing else. The advisory is named after the security impact; the commit is named after one of the dressing changes.\n\nThree things changed in the diff:\n\n1. The `$argv[1]` read is now CLI-only. A `feat` change. No effect on the CVE-2026-43873 scenario, because the web SAPI never read `$argv[1]` in a way that mattered.\n2. The string comparison is now `hash_equals`. A `feat` change. No effect on the CVE-2026-43873 scenario, because the comparison's timing channel is unreachable from the unauthenticated web path.\n3. The `echo` on the reject branch is gone. A `fix` change. The CVE record.\n\nA reader of the commit message learns that command-line input is now sanitized. A reader of the diff learns that one of the three changes is the removal of a line that printed the secret to the HTTP response. WWBN's GHSA text describes the bug accurately. The commit log next to the change does not.\n\nThe 2019 commit that introduced the line, [`a6ccbc3b`](https://github.com/WWBN/AVideo/commit/a6ccbc3bbf929455a82b59f0e6bcb26c242ccce5), changed thirty-five lines of `cloneClient.json.php` and bundled in social-share buttons elsewhere in the same change. The diff that added the echo:\n\n```diff\n-if (!User::isAdmin()) {\n- $resp->msg = \"You cant do this\";\n- $log->add(\"Clone: {$resp->msg}\");\n- die(json_encode($resp));\n+$obj = YouPHPTubePlugin::getObjectDataIfEnabled(\"CloneSite\");\n+\n+if (empty($obj) || empty($argv[1]) || $obj->myKey !== $argv[1]) {\n+ if (!User::isAdmin()) {\n+ $resp->msg = \"You cant do this\";\n+ $log->add(\"Clone: {$resp->msg}\");echo \"$obj->myKey !== $argv[1]\";\n+ die(json_encode($resp));\n+ }\n }\n```\n\nThe new statement on the formatted line reads `$log->add(\"Clone: {$resp->msg}\");echo \"$obj->myKey !== $argv[1]\";`. Two statements, semicolon, no space, no newline. This is the formatting of a print added in place to verify a code path during development, committed without cleanup. The intent of the 2019 change was to add a CLI-callable auth mode for cron-driven clones. The print was the developer checking that the new comparison rejected the caller correctly. The comparison did. The print also did what it said.\n\nThe file was re-touched eight months later in commit [`d976f5b`](https://github.com/WWBN/AVideo/commit/d976f5b2b9343754ddf5527a04748d0d4834edd7), a rename pass that changed every reference to `$obj` to `$objClone` throughout the function, including the echo line. The rename touched the offending statement. The reviewer touched the offending statement. The echo survived the touch.\n\n## The reject branch is the path nobody re-reads\n\n`User::isAdmin() === false` is the failure case of the auth check. By construction it is the path no developer exercises after first writing it, because nobody asks \"what does the rejection look like in the response body?\" once they have confirmed the rejection happens. Tests, when they exist, assert that the unauthenticated call returns the error and stop reading the body. Manual QA, when it happens, opens an admin session and exercises the success path. The reject branch in `cloneClient.json.php` was exercised seven years and fifteen days by attackers who knew what to read and zero times by the project's developers who knew what was in it.\n\nThis is the [Rejection Echoes The Secret](/patterns/rejection-echoes-the-secret) shape, named for what it does: an auth check that fails the caller, then writes the expected value into the response body before terminating. The reject branch performs the comparison, computes the value the caller should have sent, and emits it. The bug originates as a debug print added during initial development to verify the comparison works, and ships because the reject branch is the path no developer re-reads.\n\nIt is also the same shape as [todo-that-shipped](/patterns/todo-that-shipped) with the artifact swapped. There, the fix is in a `// TODO` comment and the code shipped without it. Here, the bug is in an `echo` and the code shipped with it. Both originate in the same developer state, the moment of writing the path the first time, leaving a placeholder for future cleanup, moving on to the next feature, and both ship because the path is not the path anyone walks back to.\n\nThe pattern at the architectural level is [trust-inversion](/patterns/trust-inversion). `myKey` sits in the trusted lane of every CloneSite peer relationship. Validation on the distrusted lane (the inbound HTTP request to `cloneClient.json.php`) is not the gate, because the gate is on the peer, and the peer trusts the local's identity by the local's possession of `myKey`. The local's leak is the peer's compromise. The local was not asked whether the leak was safe; the local was running an `echo` in its reject branch because a developer added it in April 2019 to confirm a key check worked. The peer is on the receiving end of seven years of impersonation potential because the local's reject branch was not designed as part of the peer's threat model and the peer's mysqldump path was not designed as part of the local's.\n\n## The reject branch was the receipts\n\nThe federation key authorizes the federation peer. The reject branch echoed the federation key. The reject branch was reachable without authentication. The patch is one line removed. The commit message does not mention the line. The release notes do not mention the file.\n\nPoC: [GHSA-qm9p-p5pw-jrx2](https://github.com/WWBN/AVideo/security/advisories/GHSA-qm9p-p5pw-jrx2)","closing_line":"The reject branch is the auth check. It told the caller what would have passed it.","hook_md":"CVE-2026-43873 is one line removed from `plugin/CloneSite/cloneClient.json.php` in WWBN AVideo. The line was `echo \"$objClone->myKey !== $argv[1]\";`. It sat inside the reject branch of an auth check that runs on every unauthenticated request to the endpoint. `$objClone->myKey` is the federation secret the local server uses to authenticate itself to its CloneSite peers. The line was added on April 9, 2019, in commit `a6ccbc3b`, titled *Fix clone site, add more shared social buttons*. It was removed on April 24, 2026, in commit `e6566f5`, titled *feat: Sanitize command line input for clone key validation*. Neither commit message mentions the line. It printed the secret into the HTTP response body for seven years and fifteen days.\nThe reject branch is the auth check. It told the caller what would have passed it.\nThe reject branch in `cloneClient.json.php` was exercised seven years and fifteen days by attackers who knew what to read and zero times by the project's developers who knew what was in it.","post_id":335,"slug":"avideo-cve-2026-43873-reject-branch-echoes-the-key","title":"CVE-2026-43873: AVideo's Reject Branch Has Been Echoing the Federation Key Since 2019","type":"initial","unreadable_sentence":"The reject branch in `cloneClient.json.php` was exercised seven years and fifteen days by attackers who knew what to read and zero times by the project's developers who knew what was in it."} -----BEGIN PGP SIGNATURE----- iHQEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCavn0DgAKCRDeZjl4jgkQ JlJtAPjIi+ksck2NZ5uvIJr8HCEcaydZKwIZQhb8b6fWEaOgAQDDuNaObUKxtL2y d4SNIJAa8ufMBVZ9V0tVoEBRpqhRAA== =FMM9 -----END PGP SIGNATURE-----