GitHub Security Advisory: GHSA-qm9p-p5pw-jrx2
Package: wwbn/avideo (COMPOSER)
Severity: HIGH (CVSS 7.5)
CWE: CWE-209: Generation of Error Message Containing Sensitive Information
Vulnerable versions: <= 29.0
Patched in: unpatched

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

`plugin/CloneSite/cloneClient.json.php` echoes the local CloneSite shared secret (`$objClone->myKey`, 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.

## Details

### 1. The leak (`plugin/CloneSite/cloneClient.json.php:51-60`)

```php
$objCloneOriginal = $objClone;
$argv[1] = preg_replace("/[^A-Za-z0-9 ]/", '', empty($argv[1])?'':$argv[1]);

if (empty($objClone) || empty($argv[1]) || $objClone->myKey !== $argv[1]) {
    if (!User::isAdmin()) {
        $resp->msg = "You can't do this";
        $log->add("Clone: {$resp->msg}");
        echo "$objClone->myKey !== $argv[1]";   // <-- interpolates myKey
        die(json_encode($resp));
    }
}
```

Under 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):

```
bool(false)                # isset($argv)
string(9) "undefined"      # $argv ?? 'undefined'
string(9) "undefined"      # $_SERVER['argv']
string(9) "undefined"      # $argv[1]
bool(true)                 # empty($argv[1])
```

Because `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->myKey` into the response body before `die()`. The response body looks like:

```
<32-hex-char md5> !== {"error":true,"msg":"You can't do this"}
```

The 32-hex prefix is the local `myKey`.

### 2. Where `myKey` comes from (`plugin/CloneSite/CloneSite.php:67`)

```php
$obj->myKey = md5($global['systemRootPath'].$global['salt']);
```

`myKey` is a static per-installation value generated from `systemRootPath` and `salt`. It never rotates.

### 3. Why the leaked key is dangerous (cross-site chain)

`cloneClient.json.php:75` shows `myKey` is the credential the client presents to its configured remote clone server:

```php
$url = $objClone->cloneSiteURL . "plugin/CloneSite/cloneServer.json.php?url="
     . urlencode($global['webSiteRootURL']) . "&key={$objClone->myKey}&useRsync=" . intval($objClone->useRsync);
```

On the remote side, `plugin/CloneSite/cloneServer.json.php:32-42` calls `Clones::thisURLCanCloneMe($_GET['url'], $_GET['key'])`, which in `plugin/CloneSite/