GitHub Security Advisory: GHSA-jw8g-5j46-44rp
Package: wwbn/avideo (COMPOSER)
Severity: MODERATE (CVSS 5.4)
CWE: CWE-352: Cross-Site Request Forgery (CSRF)
Vulnerable versions: <= 29.0
Patched in: unpatched

Summary: AVideo: CSRF in userSavePhoto.php Allows Cross-Origin Overwrite of Authenticated Users' Profile Photos with Arbitrary Content

Details:
## Summary

`objects/userSavePhoto.php` is a legacy profile-photo endpoint that accepts a base64 POST parameter and writes the decoded bytes to `videos/userPhoto/photo<users_id>.png`. Its only access control is `User::isLogged()`. It does not end in `.json.php`, so it is excluded from the project's global `autoCSRFGuard` (which is suffix-scoped in `objects/include_config.php`). There is no CSRF token, no Origin/Referer check, and no MIME validation of the decoded bytes. Because AVideo's default cookie policy is `SameSite=None; Secure` on HTTPS (`objects/functionsPHP.php:227`), an attacker who lures a logged-in user to a malicious page can overwrite that user's profile photo with arbitrary bytes and also triggers a site-wide `clearCache(true)` on every forged request.

## Details

Handler (`objects/userSavePhoto.php`, 51 lines total):

```php
// line 12 - only access control
if (!User::isLogged()) {
    $obj->msg = __("You must be logged");
    die(json_encode($obj));
}
// ...
// line 29 - unvalidated base64 from POST
$fileData = base64DataToImage($_POST['imgBase64']);
// line 30 - deterministic filename tied to the VICTIM's session
$fileName = 'photo'. User::getId().'.png';
$photoURL = $imagePath.$fileName;
// line 35 - raw bytes written to disk
$bytes = file_put_contents($global['systemRootPath'].$photoURL, $fileData);
// lines 43-48 - DB update + global cache invalidation unconditionally
$user = new User(User::getId());
$user->setPhotoURL($photoURL);
if ($user->save()) {
    User::deleteOGImage(User::getId());
    User::updateSessionInfo();
    clearCache(true);
}
```

`base64DataToImage` (`objects/functionsImages.php:1026`) performs no content validation:

```php
function base64DataToImage($imgBase64) {
    $img = $imgBase64;
    $img = str_replace('data:image/png;base64,', '', $img);
    $img = str_replace(' ', '+', $img);
    return base64_decode($img);
}
```

There is no call to `getimagesizefromstring`, `imagecreatefromstring`, or MIME detection. Arbitrary bytes up to `post_max_size` are accepted.

**Why the global CSRF guard does not apply.** `objects/include_config.php` (around line 314) only invokes `autoCSRFGuard` when the script filename matches `*.json.php`:

```php
if (... $_SERVER['REQUEST_METHOD'] === 'POST' &&
    substr($baseName, -9) === '.json.php') {
    autoCSRFGuard($baseName, $_SERVER['SCRIPT_FILENAME']);
}
```

`userSavePhoto.php` is missing the `.json.php` suffix, so neither `autoCSRFGuard` nor `forbidIfIsUntrustedRequest` runs. There is no explicit call to any of these in the file (verified by grep: no `getCSRF`, no `forbidIfIsUntrustedRequest`, no `HTTP_ORIGIN`, no `HTTP_REFERER`). Routing rewrites in `.htaccess` also expose this handler as `/savePhoto`.

**Why the victim's cookie is sent cross-origin.** `objects/functionsPHP.php:227`:

```php
function _getCookieSameSiteValue($secure) {
    return $secure ? 'None' : 'Lax';
}
```

On HTTPS (the expected deployment), session cookies default to `SameSite=None; Secure`, wh