GitHub Security Advisory: GHSA-wp38-whx3-xffh
Package: wwbn/avideo (COMPOSER)
Severity: MODERATE (CVSS 5.4)
CWE: CWE-918: Server-Side Request Forgery (SSRF)
Vulnerable versions: <= 29.0
Patched in: unpatched

Summary: AVideo has Blind SSRF in YPTWallet Donation Webhook via Missing isSSRFSafeURL() Check and CURLOPT_FOLLOWLOCATION Redirect Bypass

Details:
## Summary

An authenticated user can configure their own donation-notification webhook URL to point at internal/loopback/metadata hosts (e.g. `http://127.0.0.1:8080/...`, `http://169.254.169.254/latest/...`, RFC1918 addresses). When any other user (including a second account owned by the same attacker) donates even a trivial amount via `plugin/CustomizeUser/donate.json.php`, the AVideo server issues a `curl` POST to the attacker-supplied URL, resulting in a blind SSRF. The handler uses only `isValidURL()` (which is a format check) and does not call the codebase's own `isSSRFSafeURL()` helper. Additionally, `CURLOPT_FOLLOWLOCATION` is enabled with no per-hop revalidation, so even if the stored URL were validated, an HTTP 307 from an attacker-controlled host could redirect the POST to internal targets.

## Details

### Sink: unvalidated curl POST in afterDonation

`plugin/YPTWallet/YPTWallet.php:1043-1099`

```php
public function afterDonation($from_users_id, $how_much, $videos_id, $users_id, $extraParameters)
{
    ...
    $donation_notification_url = self::getDonationNotificationURL($users_id);
    ...
    if (!empty($donation_notification_url) && isValidURL($donation_notification_url)) {
        ...
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $donation_notification_url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 1);
        curl_setopt($ch, CURLOPT_NOSIGNAL, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);   // line 1081
        ...
        ob_start();
        curl_exec($ch);
        ob_end_clean();
```

The gate at line 1064 is `isValidURL()` only. That helper is a pure format check:

`objects/functions.php:4305-4315`

```php
function isValidURL($url)
{
    if (empty($url) || !is_string($url)) {
        return false;
    }
    if (preg_match("/^http.*/", $url) && filter_var($url, FILTER_VALIDATE_URL)) {
        return true;
    }
    return false;
}
```

It does not reject `http://127.0.0.1`, `http://169.254.169.254`, RFC1918 ranges, or hostnames that resolve to private IPs.

The project already ships a correct SSRF guard at `objects/functions.php:4366` (`isSSRFSafeURL()`), which performs scheme allow-listing, hostname-to-IP resolution, loopback blocking, RFC1918 / link-local / metadata blocking, and IPv4-mapped IPv6 normalization. It is not used here.

### Storage path has no SSRF validation

`plugin/YPTWallet/view/saveConfiguration.php:31-33`

```php
if(isset($_REQUEST['donation_notification_url'])){
     $obj->donation_notification_url = YPTWallet::setDonationNotificationURL(User::getId(), $_REQUEST['donation_notification_url']);
}
```

`plugin/YPTWallet/YPTWallet.php:1015-1034`

```php
static function setDonationNotificationURL($users_id, $url)
{
    $url = trim($url);
    $url = preg_replace('/[\x00-