Skip to content

Commit 788b989

Browse files

File tree

5 files changed

+313
-0
lines changed

5 files changed

+313
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-cmcr-q4jf-p6q9",
4+
"modified": "2026-04-08T00:08:47Z",
5+
"published": "2026-04-08T00:08:47Z",
6+
"aliases": [
7+
"CVE-2026-39370"
8+
],
9+
"summary": "WWBN AVideo has an Allowlisted downloadURL media extensions bypass SSRF protection and enable internal response exfiltration (Incomplete fix for CVE-2026-27732)",
10+
"details": "## Summary\n\nThe fix for [CVE-2026-27732](https://github.com/WWBN/AVideo/security/advisories/GHSA-h39h-7cvg-q7j6) is incomplete.\n\n`objects/aVideoEncoder.json.php` still allows attacker-controlled `downloadURL` values with common media or archive extensions such as `.mp4`, `.mp3`, `.zip`, `.jpg`, `.png`, `.gif`, and `.webm` to bypass SSRF validation. The server then fetches the response and stores it as media content.\n\nThis allows an authenticated uploader to turn the upload-by-URL flow into a reliable SSRF response-exfiltration primitive.\n\n## Details\n\n`objects/aVideoEncoder.json.php` accepts attacker-controlled `downloadURL` and passes it to `downloadVideoFromDownloadURL()`.\n\nInside that function:\n\n1. the URL extension is extracted from the attacker-controlled path\n2. the extension is checked against an allowlist of normal encoder formats\n3. `isSSRFSafeURL()` is skipped for common media and archive extensions\n4. the URL is fetched via `url_get_contents()`\n5. the fetched body is written into video storage and exposed through normal media metadata\n\nThe current code still contains:\n\n- an extension-based bypass for SSRF validation\n- no mandatory initial-destination SSRF enforcement inside `url_get_contents()` itself\n\nThis means internal URLs such as:\n\n`http://127.0.0.1:9998/probe.mp4`\n\nremain reachable from the application host.\n\nThis issue is best described as an incomplete fix / patch bypass of `CVE-2026-27732`, not a separate unrelated SSRF class.\n\n## Proof of concept\n\n1. Log in as a low-privilege uploader.\n2. Start an HTTP service reachable only from inside the application environment, for example:\n\n```text\nhttp://127.0.0.1:9998/probe.mp4\n```\n\n3. Confirm that the service is not reachable externally.\n4. Send:\n\n```text\nPOST /objects/aVideoEncoder.json.php\ndownloadURL=http://127.0.0.1:9998/probe.mp4\nformat=mp4\n```\n\n5. If needed, replay once against the returned `videos_id` with `first_request=1` so the fetched bytes land in the normal media path.\n6. Query:\n\n```text\nGET /objects/videos.json.php?showAll=1\n```\n\n7. Recover `videosURL.mp4.url`.\n8. Download that media URL and observe that the body matches the internal-only response byte-for-byte.\n\n## Impact\n\nAn authenticated uploader can make the AVideo server fetch loopback or internal HTTP resources and persist the response as media content by supplying a `downloadURL` ending in an allowlisted extension such as `.mp4`, `.jpg`, `.gif`, or `.zip`. Because SSRF validation is skipped for those extensions, the fetched body is stored and later retrievable through the generated `/videos/...` media URL. Successful exploitation allows internal response exfiltration from private APIs, admin endpoints, or other internal services reachable from the application host.\n\n\n## Recommended fix\n\n- Apply `isSSRFSafeURL()` to all `downloadURL` inputs regardless of extension\n- Remove extension-based exceptions from SSRF enforcement\n- Move initial-destination SSRF validation into `url_get_contents()` so call sites cannot skip it\n- Avoid storing arbitrary fetched content directly as publicly retrievable media\n- Consider restricting upload-by-URL to an explicit allowlist of trusted fetch origins",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Packagist",
21+
"name": "WWBN/AVideo"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"last_affected": "26.0"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-cmcr-q4jf-p6q9"
42+
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39370"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/WWBN/AVideo"
50+
}
51+
],
52+
"database_specific": {
53+
"cwe_ids": [
54+
"CWE-918"
55+
],
56+
"severity": "HIGH",
57+
"github_reviewed": true,
58+
"github_reviewed_at": "2026-04-08T00:08:47Z",
59+
"nvd_published_at": "2026-04-07T20:16:31Z"
60+
}
61+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-f4f9-627c-jh33",
4+
"modified": "2026-04-08T00:08:44Z",
5+
"published": "2026-04-08T00:08:44Z",
6+
"aliases": [
7+
"CVE-2026-39369"
8+
],
9+
"summary": "WWBN AVideo's GIF poster fetch bypasses traversal scrubbing and exposes local files through public media URLs",
10+
"details": "## Summary\n\n`objects/aVideoEncoderReceiveImage.json.php` allowed an authenticated uploader to fetch attacker-controlled same-origin `/videos/...` URLs, bypass traversal scrubbing, and expose server-local files through the GIF poster storage path.\n\nThe vulnerable GIF branch could be abused to read local files such as `/etc/passwd` or application source files and republish those bytes through a normal public GIF media URL.\n\n## Details\n\nThe vulnerable chain was:\n\n1. `objects/aVideoEncoderReceiveImage.json.php` accepted attacker-controlled `downloadURL_gifimage`\n2. traversal scrubbing used `str_replace('../', '', ...)`, which was bypassable with overlapping input such as `....//`\n3. same-origin `/videos/...` URLs were accepted\n4. `url_get_contents()` and `try_get_contents_from_local()` resolved the request into a local filesystem read\n5. the fetched bytes were written into the GIF destination\n6. invalid GIF cleanup used the wrong variable, so the non-image payload remained on disk\n\nThis made the GIF poster path a local file disclosure primitive with public retrieval.\n\n## Proof of concept\n\n1. Log in as an uploader and create an owned video row through the normal encoder flow.\n2. Send:\n\n```text\nPOST /objects/aVideoEncoderReceiveImage.json.php\ndownloadURL_gifimage=https://localhost/videos/....//....//....//....//....//....//etc/passwd\n```\n\n3. Query:\n\n```text\nGET /objects/videos.json.php?showAll=1\n```\n\n4. Recover the generated GIF URL from `videosURL.gif.url`.\n5. Download that GIF URL.\n6. Observe that the body matches the target local file, such as `/etc/passwd`, byte-for-byte.\n\n## Impact\n\nAn authenticated uploader can read server-local files and republish them through a public GIF media URL by supplying a crafted same-origin `/videos/...` path to `downloadURL_gifimage`. Because traversal scrubbing was bypassable and the fetched bytes were written to the GIF destination without effective invalid-image cleanup, successful exploitation allows disclosure of files such as `/etc/passwd`, readable application source code, or deployment-specific configuration accessible to the application.\n\n\n## Recommended fix\n\n- Reject any remote image URL whose decoded path contains traversal markers\n- Do not allow attacker-controlled same-origin `/videos/...` fetches to resolve into local file reads\n- Constrain any local shortcut path handling with `realpath()` and strict base-directory allowlists\n- Validate GIF content before saving it into public media storage\n- Ensure invalid-image cleanup checks the correct destination path",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Packagist",
21+
"name": "WWBN/AVideo"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"last_affected": "26.0"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-f4f9-627c-jh33"
42+
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39369"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/WWBN/AVideo"
50+
}
51+
],
52+
"database_specific": {
53+
"cwe_ids": [
54+
"CWE-22"
55+
],
56+
"severity": "HIGH",
57+
"github_reviewed": true,
58+
"github_reviewed_at": "2026-04-08T00:08:44Z",
59+
"nvd_published_at": "2026-04-07T20:16:31Z"
60+
}
61+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-mmw7-wq3c-wf9p",
4+
"modified": "2026-04-08T00:08:33Z",
5+
"published": "2026-04-08T00:08:33Z",
6+
"aliases": [
7+
"CVE-2026-39366"
8+
],
9+
"summary": "WWBN AVideo Affected by a PayPal IPN Replay Attack Enabling Wallet Balance Inflation via Missing Transaction Deduplication in ipn.php",
10+
"details": "## 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->currency===$payment_currency) {\n $plugin->addBalance($users_id, $payment_amount, \"Paypal recurrent\", json_encode($_POST));\n Subscription::renew($users_id, $row['subscriptions_plans_id']);\n $obj->error = 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 > Settings > 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&mc_gross=9.99&mc_currency=USD&payment_status=Completed&txn_type=recurring_payment&verify_sign=REAL_VERIFY_SIGN&payer_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&mc_gross=9.99&mc_currency=USD&payment_status=Completed&txn_type=recurring_payment&verify_sign=REAL_VERIFY_SIGN&payer_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->currency===$payment_currency) {\n // Log the transaction for deduplication\n $pp = new PayPalYPT_log(0);\n $pp->setUsers_id($users_id);\n $pp->setRecurring_payment_id($dedup_key);\n $pp->setValue($payment_amount);\n $pp->setJson(['post' => $_POST]);\n if ($pp->save()) {\n $plugin->addBalance($users_id, $payment_amount, \"Paypal recurrent\", json_encode($_POST));\n Subscription::renew($users_id, $row['subscriptions_plans_id']);\n $obj->error = false;\n }\n } else {\n _error_log(\"PayPalIPN: FAIL currency check $walletObject->currency===$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.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Packagist",
21+
"name": "wwbn/avideo"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"last_affected": "26.0"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-mmw7-wq3c-wf9p"
42+
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39366"
46+
},
47+
{
48+
"type": "WEB",
49+
"url": "https://github.com/WWBN/AVideo/commit/8f53e9d9c6aaa07d51ace30691981edbbfb5ca1c"
50+
},
51+
{
52+
"type": "PACKAGE",
53+
"url": "https://github.com/WWBN/AVideo"
54+
}
55+
],
56+
"database_specific": {
57+
"cwe_ids": [
58+
"CWE-345"
59+
],
60+
"severity": "MODERATE",
61+
"github_reviewed": true,
62+
"github_reviewed_at": "2026-04-08T00:08:33Z",
63+
"nvd_published_at": "2026-04-07T20:16:30Z"
64+
}
65+
}

0 commit comments

Comments
 (0)